スコープとホイスト
var a = 1; function foo() { if (!a) { var a = 2; } alert(a); }; foo();
上記のコードを実行するとどのような結果が生成されますか?
これは経験豊富なプログラマーにとっては朝飯前ですが、それでも初心者の一般的な考えに従って説明します。
1. グローバル変数 a を作成し、その値を 1 として定義します
2. 関数 foo
を作成しました
3. foo の関数本体では、!a が変数 a をブール値 false、つまり false
に変換するため、if ステートメントは実行されません。
4. 条件分岐をスキップし、変数 a を警告します。最終結果は 1
これは完璧な推論のように見えますが、驚くべきことに、答えは実際には 2 です。なぜ?
心配しないでください、私が説明します。最初に言っておきますが、これはバグではなく、JavaScript 言語インタープリターの (非公式) 機能であり、誰か (Ben Cherry) はこの機能を ホイスティング< と呼んでいました。 🎜> (標準的な翻訳はまだありません。より一般的な翻訳は 宣伝 です)。
宣言と定義
ホイスティングを理解するために、まず簡単な状況を見てみましょう:var a = 1;
上記のコードが実行されると正確に何が起こるか考えたことがありますか?
このコードに関する限り、「変数 a を宣言する」と「変数 a を定義する」という 2 つのステートメントのどちらが正しいかわかりますか?
•次の例は「変数の宣言」と呼ばれます:
•次の例は「変数の定義」と呼ばれます:
var a = 1;
•宣言: 変数や関数などの何かの存在を主張することを意味しますが、それが何であるかを説明せず、そのようなものが存在することをインタープリターに伝えるだけです。 •定義: 変数の値が何であるか、関数の関数本体が何であるかなど、何かの具体的な実装を指定し、そのようなものの意味を正確に表現することを意味します。
var a; // これはステートメントです
a = 1; // これが定義 (代入)
var a = 1; // 2 つを 1 つに結合します: 変数の存在を宣言し、それに値を割り当てます
ここが重要なポイントです: 1 つのこと (var a = 1) だけを行ったと思った場合、インタプリタは実際にはこれを 2 つのステップに分解します。1 つは宣言 (var a)、もう 1 つは定義 ( a = 1)。
これはホイスティングと何の関係がありますか?
冒頭のわかりにくい例に戻り、インタプリタがコードをどのように分析するかを説明します。
var a; a = 1; function foo() { var a; // 关键在这里 if (!a) { a = 2; } alert(a); // 此时的 a 并非函数体外的那个全局变量 }
「if ステートメント内で変数 a を宣言しないのはなぜですか?」と尋ねる人もいるかもしれません。
JavaScript にはブロック スコープ (Block Scoping) がなく、関数スコープ (Function Scoping) しかないため、中括弧 {} のペアが表示された場合は、新しいスコープが生成されたことを意味し、C とは異なります。 !
パーサーは if ステートメントを読み取ると、変数の宣言と代入があることを認識するため、パーサーはその宣言を現在のスコープの先頭に引き上げます (これはデフォルトの動作であり、変更できません)。この動作はホイスティングと呼ばれます。
わかった、みんなわかった、わかった...
理解したからといって、それを使用できるわけではありません。最初の例を見てみましょう。alert(a) で 1 を生成したいだけの場合、どうすればよいでしょうか?
新しいスコープの作成alert(a) が実行されると、変数 a の場所が検索されます。現在のスコープから最上位のスコープまで上方向 (または外側) に検索され、見つからない場合は、未定義と報告されます。 。
alert(a) の兄弟スコープでローカル変数 a を再度宣言したため、2 が報告され、ローカル変数 a の宣言を下方向 (または内側) に移動できるため、alert (a)見つかりません。
覚えておいてください: JavaScript には関数スコープしかありません。
你或许在无数的 JavaScript 书籍和文章里读到过:“请始终保持作用域内所有变量的声明放置在作用域的顶部”,现在你应该明白为什么有此一说了吧?因为这样可以避免 Hoisting 特性给你带来的困扰(我不是很情愿这么说,因为 Hoisting 本身并没有什么错),也可以很明确的告诉所有阅读代码的人(包括你自己)在当前作用域内有哪些变量可以访问。但是,变量声明的提升并非 Hoisting 的全部。在 JavaScript 中,有四种方式可以让命名进入到作用域中(按优先级):
1.语言定义的命名:比如 this 或者 arguments,它们在所有作用域内都有效且优先级最高,所以在任何地方你都不能把变量命名为 this 之类的,这样是没有意义的
2.形式参数:函数定义时声明的形式参数会作为变量被 hoisting 至该函数的作用域内。所以形式参数是本地的,不是外部的或者全局的。当然你可以在执行函数的时候把外部变量传进来,但是传进来之后就是本地的了
3.函数声明:函数体内部还可以声明函数,不过它们也都是本地的了
4.变量声明:这个优先级其实还是最低的,不过它们也都是最常用的
另外,还记得之前我们讨论过 声明 和 定义 的区别吧?当时我并没有说为什么要理解这个区别,不过现在是时候了,记住:
Hosting 只提升了命名,没有提升定义
这一点和我们接下来要讲到的东西息息相关,请看:
函数声明与函数表达式的差别
先看两个例子:
function test() { foo(); function foo() { alert("我是会出现的啦……"); } } test();
function test() { foo(); var foo = function() { alert("我不会出现的哦……"); } } test();
同学,在了解了 Scoping & Hoisting 之后,你知道怎么解释这一切了吧?
在第一个例子里,函数 foo 是一个声明,既然是声明就会被提升(我特意包裹了一个外层作用域,因为全局作用域需要你的想象,不是那么直观,但是道理是一样的),所以在执行 foo() 之前,作用域就知道函数 foo 的存在了。这叫做函数声明(Function Declaration),函数声明会连通命名和函数体一起被提升至作用域顶部。
然而在第二个例子里,被提升的仅仅是变量名 foo,至于它的定义依然停留在原处。因此在执行 foo() 之前,作用域只知道 foo 的命名,不知道它到底是什么,所以执行会报错(通常会是:undefined is not a function)。这叫做函数表达式(Function Expression),函数表达式只有命名会被提升,定义的函数体则不会。
尾记:Ben Cherry 的原文解释的更加详细,只不过是英文而已。我这篇是借花献佛,主要是更浅显的解释给初学者听,若要看更多的示例,请移步原作,谢谢。