次のプログラムの結果はどうなるでしょうか?
次のようにコードをコピーします。
var foo = 1;
関数 bar() {
if (!foo) {
var foo = 10;
}
アラート(foo);
}
バー();
結果は 10 です。
これはどうでしょうか?
次のようにコードをコピーします。
変数 a = 1;
関数 b() {
a = 10;
戻る;
関数 a() {}
}
b();
アラート(a);
結果は 1 です。
怖いですか?どうしたの?これは奇妙で、危険で、混乱を招くかもしれませんが、実際には非常に便利で印象的な JavaScript 言語機能でもあります。この動作に標準的な名前があるかどうかはわかりませんが、私はこの「ホイスティング」という用語が好きです。この記事ではこの仕組みについて入門的に説明しますが、その前に JavaScript の範囲について必要な理解をしておきましょう。
JavaScriptの適用範囲
Javascript の初心者にとって、最も混乱する領域の 1 つはスコープです。実際、それは初心者だけではありません。私は何人かの経験豊富な JavaScript プログラマーに会ったことがありますが、彼らはスコープについて深く理解していません。 JavaScript スコープがわかりにくい理由は、次の C プログラムのように、プログラム構文自体が C ファミリ言語に似ているためです。
次のようにコードをコピーします。
#include <stdio.h>
int main() {
int x = 1;
printf("%d, ", x);
if (1) {
int x = 2;
printf("%d, ", x);
}
printf("%d/n", x);
}
出力結果は 1 2 1 になります。これは、C 系言語にはブロックスコープがあるため、プログラム制御が if ブロックなどのブロックに入る場合、ブロック外の効果には影響を与えず、そのブロック内にのみ影響を与える変数を宣言できます。ドメイン。しかし、JavaScript ではこれは機能しません。以下のコードを見てください。
次のようにコードをコピーします。
var x = 1;
コンソール.log(x); // 1
if (true) {
var x = 2;
コンソール.log(x); // 2
}
コンソール.log(x); // 2
結果は 1 2 2 になります。 JavaScriptは関数スコープなので。これが C 言語ファミリーとの最大の違いです。このプログラムの if は新しいスコープを作成しません。
多くの C、C++、Java プログラマにとって、これは期待し歓迎することではありません。幸いなことに、JavaScript 関数の柔軟性により、これを回避する方法があります。一時的なスコープを作成する必要がある場合は、次のようにします。
次のようにコードをコピーします。
関数 foo() {
var x = 1;
if (x) {
(関数 () {
var x = 2;
// 他のコード
}());
}
// x は 1 のままです。
}
この方法は柔軟で、一時的なスコープを作成したい場所ならどこでも使用できます。ブロック内だけではありません。ただし、時間をかけて JavaScript のスコープを理解することを強くお勧めします。これは非常に便利で、JavaScript の私のお気に入りの機能の 1 つです。スコープを理解していれば、変数のホイスティングがより理解できるでしょう。
変数の宣言、命名、昇格
JavaScript では、変数がスコープに入るには 4 つの基本的な方法があります。
•1 組み込み言語: すべてのスコープに this と引数があります (翻訳者注: テスト後、引数はグローバル スコープでは表示されません)。
•2 仮パラメータ: 関数の仮パラメータは、関数本体のスコープの一部になります。
•3 関数宣言: 次の形式のように: function foo(){};
•4 変数宣言: 次のように: var foo;
関数宣言と変数宣言は、インタプリタによって常に静かにメソッド本体の先頭に「上げられ」ます。つまり、次のようなコードになります。
次のようにコードをコピーします。
関数 foo() {
バー();
var x = 1;
}
実際には次のように解釈されます。
次のようにコードをコピーします。
関数 foo() {
変数x;
バー();
x = 1;
}
変数が定義されているブロックが実行可能かどうかは関係ありません。次の 2 つの関数は実際には同じものです。
次のようにコードをコピーします。
関数 foo() {
if (偽) {
var x = 1;
}
戻る;
変数y = 1;
}
関数 foo() {
変数x、y;
if (偽) {
x = 1;
}
戻る;
y = 1;
}
変数の代入はホイストされず、宣言のみであることに注意してください。ただし、関数の宣言は少し異なり、関数本体も昇格されます。ただし、関数を宣言するには 2 つの方法があることに注意してください。
次のようにコードをコピーします。
関数テスト() {
foo(); // TypeError "foo は関数ではありません"
bar(); // 「これは実行されます!」
var foo = function () { // 変数は関数式を指します
alert("これは実行されません!");
}
function bar() { // bar という名前の関数宣言関数
alert("これは実行されます!");
}
}
テスト();
この例では、関数宣言のみが関数本体とともにホイストされます。 foo の宣言はホイストされますが、それが指す関数本体は実行中にのみ割り当てられます。
上記ではブーストの基本の一部を説明していますが、それほど混乱するものではないようです。ただし、一部の特殊なシナリオでは、依然としてある程度の複雑さが存在します。
変数の解析順序
留意すべき最も重要なことは、変数の解決順序です。先ほど説明した、命名が範囲に入る 4 つの方法を覚えていますか?変数が解析される順序は、リストした順序です。
次のようにコードをコピーします。
<スクリプト>
関数 a(){
}
変数a;
alert(a);//a の関数本体を出力します。
</script>
<スクリプト>
変数a;
関数 a(){
}
alert(a);//a の関数本体を出力します。
</script>
//ただし、次の 2 つの記述方法の違いに注意してください。
<スクリプト>
変数a=1;
関数 a(){
}
alert(a);//出力1
</script>
<スクリプト>
関数 a(){
}
変数a=1;
alert(a);//出力1
</script>
ここには 3 つの例外があります。
1 組み込みの名前引数は、関数の仮パラメータの後、関数宣言の前に宣言する必要があるようです。これは、仮パラメータに引数がある場合、それが組み込みパラメータよりも優先されることを意味します。これは非常に悪い機能なので、仮パラメータで引数を使用することは避けてください。
2 この変数を任意の場所で定義すると構文エラーが発生しますが、これは良い機能です。
3. 複数の仮パラメータが同じ名前である場合、実際の動作中にその値が不定であっても、最後の仮パラメータが優先されます。
名前付き関数
関数に名前を付けることができます。その場合、それは関数宣言ではなく、関数本体定義内の指定された関数名 (もしあれば、以下のスパムなど、翻訳者注記) は昇格されず、無視されます。理解に役立つコードをいくつか示します。
次のようにコードをコピーします。
foo(); // TypeError "foo は関数ではありません"
bar(); // 有効です
baz(); // TypeError "baz は関数ではありません"
spam(); // ReferenceError "スパムが定義されていません"
var foo = function () {} // foo は匿名関数を指します。
function bar() {} // 関数の宣言
var baz = function spam() {}; // 名前付き関数。baz のみが昇格され、スパムは昇格されません。
foo(); // 有効
bar(); // 有効です
baz(); // 有効
spam(); // ReferenceError "スパムが定義されていません"
コードの書き方
スコープと変数ホイスティングについては理解できましたが、これは JavaScript コーディングにとって何を意味するのでしょうか?最も重要なことは、変数を常に var で定義することです。また、名前については、スコープ内に var 宣言を常に 1 つだけ含めることを強くお勧めします。これを行うと、スコープと変数のホイスティングの問題は発生しなくなります。
言語仕様とはどういう意味ですか?
ECMAScript リファレンス ドキュメントは常に役に立ちます。スコープと変数のホイスティングについて私が発見したことは次のとおりです。
変数が関数本体クラスで宣言されている場合、それは関数スコープになります。それ以外の場合は、(グローバルのプロパティとして) グローバルにスコープされます。変数は、実行がスコープに入ると作成されます。ブロックは新しいスコープを定義せず、関数宣言とプロシージャ (翻訳者はグローバル コード実行であると考える) のみが新しいスコープを作成します。変数は作成時に未定義に初期化されます。変数宣言ステートメントに代入操作がある場合、その代入操作は作成時ではなく実行時にのみ発生します。
この記事が、JavaScript について混乱しているプログラマーに一筋の光をもたらすことを願っています。私もこれ以上混乱を招かないように最善を尽くします。私が何か間違ったことを言ったり、何か見落としたりした場合は、お知らせください。
翻訳者の補足
友人が、IE のグローバル スコープにおける名前付き関数のプロモーションの問題について思い出させてくれました。
記事を翻訳するときにテストした方法は次のとおりです。
次のようにコードをコピーします。
<スクリプト>
functiont(){
スパム();
var baz = function spam() {alert('これはスパムです')};
}
t();
</script>
この書き方、つまり、非グローバル スコープでの名前付き関数の昇格は、ie と ff で同じパフォーマンスを発揮します。
次のようにコードをコピーします。
<スクリプト>
スパム();
var baz = function spam() {alert('これはスパムです')};
</script>
この場合、スパムは ie では実行できますが、ff では実行できません。これは、ブラウザごとにこの詳細が異なることを示しています。
この質問により、他の 2 つの質問についても考えるようになりました。 1: グローバル スコープを持つ変数の場合、var と非 var には違いがあり、var がないと変数は昇格されません。たとえば、次の 2 つのプログラムのうち、2 番目のプログラムはエラーを報告します。
次のようにコードをコピーします。
<スクリプト>
アラート(a);
変数a=1;
</script>
次のようにコードをコピーします。
<スクリプト>
アラート(a);
a=1;
</script>
2: eval で作成されたローカル変数は昇格されません (昇格する方法がありません)。
次のようにコードをコピーします。
<スクリプト>
変数 a = 1;
関数 t(){
アラート(a);
eval('var a = 2');
アラート(a);
}
t();
アラート(a);
</script>