var foo = 1; function bar() { if (!foo) { var foo = 10; } alert(foo); } bar();
「10」が表示されていることに驚いた場合、次のコードはあなたを完全に混乱させます:
var a = 1; function b() { a = 10; return; function a() {} } b(); alert(a);
この場合、ブラウザには「1」が表示されます。 一体何が起こっているのでしょうか? この振る舞いは奇妙で、危険で、紛らわしいように見えますが、実際には非常に強力で表現力豊かなJavaScriptツールです。 この動作に正式な名前があるかどうかはわかりませんが、「巻き上げ」という用語を使用したいと思います。 この記事では、この言語のメカニズムに光を当てようとしますが、最初にJavaScriptのスコープについて話しましょう。
JavaScriptスコープ
初心者が混乱する理由の1つはスコープです。 一般的に、初心者だけでなく。 JavaScriptのスコープメカニズムを理解していない経験豊富なJavaScript開発者に会いました。 その理由は、外部的には、JavaScriptは他のCライクな言語と非常に似ているからです。
次のCコードを見てみましょう。
#include <stdio.h> int main() { int x = 1; printf("%d, ", x); // 1 if (1) { int x = 2; printf("%d, ", x); // 2 } printf("%d\n", x); // 1 }
Cおよび他のすべてのCライクな言語はコードブロックのレベルでスコープを実装するため、このプログラムは1、2、1を出力します 。 if条件などの新しいコードブロックが実行されると、その中で宣言された新しい変数は外部スコープの変数に影響しません。
ただし、JavaScriptの場合はそうではありません。 Firebugでこのコードを実行してみてください。
var x = 1; console.log(x); // 1 if (true) { var x = 2; console.log(x); // 2 } console.log(x); // 2
今回は、数字1、2、2が表示されますが、これはJavaScript が関数レベルでスコープを使用しているためです 。 これは、Cのようなプログラミング言語で見慣れているものではありません。 ifの直後にあるようなコードのブロックは、新しいスコープを作成しません 。 関数のみが新しいスコープを作成します。
C、C ++、C#、またはJavaに慣れている多くのプログラマーにとって、この動作は非常に予期せず不快です。 幸いなことに、JavaScript関数の柔軟性のおかげで、この問題を回避できます。 関数内に一時的なスコープを作成するには、次のようにします。
function foo() { var x = 1; if (x) { (function () { var x = 2; // - }()); } // x 1. }
このアプローチは非常に柔軟であり、コードブロック内だけでなく、一時的なスコープが必要な場合に使用できます。 しかし、JavaScriptでのスコープの実装を理解するためにまだ時間を費やしていると私は主張します。 これは私が本当に気に入っている非常に強力な言語機能です。 スコープを理解すれば、変数と関数宣言の「発生」を理解しやすくなります。
変数と関数の宣言、命名、および「引き上げ」
JavaScriptのスコープに識別子が表示される主な方法は4つあります。
- 言語の内部メカニズム :たとえば、このスコープでは、thisおよび引数が利用可能です。
- 仮パラメータ :関数は名前付き仮パラメータを持つことができ、その範囲は関数の本体によって制限されます。
- 関数宣言:関数foo(){}として宣言されています。
- 変数宣言 :たとえば、var foo;。
JavaScriptインタープリターは、常に静かに関数と変数の宣言をスコープの最上部に移動(「レイズ」)します。 関数の正式なパラメーターと組み込みの言語変数は、明らかに最初の段階にあります。 これは、このコードが次のことを意味します。
function foo() { bar(); var x = 1; }
実際に次のように解釈されます:
function foo() { var x; bar(); x = 1; }
宣言が発生する行が実行されるかどうかは関係ありません。 次の2つの関数は同等です。
function foo() { if (false) { var x = 1; } return; var y = 1; } function foo() { var x, y; if (false) { x = 1; } return; y = 1; }
変数への値の割り当ては、それらの宣言では行われないことに注意してください。 変数宣言のみが発生します。 関数の場合、関数全体が上昇します。 関数を宣言するには、主に2つの方法があります。それらを見てみましょう。
function test() { foo(); // TypeError "foo is not a function" bar(); // "this will run!" var foo = function () { // , 'foo' alert("this won't run!"); } function bar() { // 'bar' alert("this will run!"); } } test();
この場合、bar関数のみが発生します。 識別子「foo」も上昇しますが、匿名関数ではありません-そのままです。
そこで、変数と関数の「引き上げ」の主なポイントについて説明しました。 もちろん、すべてが少し複雑になる特別なケースがなければ、JavaScriptはそれ自体ではありません。
名前解決
覚えておくべき最も重要な特別なケースは、名前解決手順です。 識別子がスコープに表示される方法は4つあることを忘れないでください。 名前解決が行われるのは、私がそれらに言及した順序です。 一般に、名前がすでに定義されている場合、同じ名前を持つ別のエンティティによって名前が再定義されることはありません。 つまり、関数宣言は同じ名前の変数宣言よりも優先されます。 しかし、これは変数に値を代入しても関数が置き換えられないことを意味するものではなく、その定義は単に無視されます。
いくつかの例外があります。
- 組み込みの引数識別子は奇妙に動作します。 これは、関数の仮引数の直後で、関数の宣言の前に宣言されているかのようです。 これは、関数が仮引数を持っている場合、関数が呼び出されたときに渡されなくても、組み込み関数よりも優先されることを意味します。 これは悪いJavaScript機能です。 argumentsという名前の仮引数を使用しないでください。
- これを識別子として使用しようとすると、SyntaxErrorエラーが発生します。 これは良い機能です。
- 関数の仮パラメータのリストでそれらのいくつかが同じ名前を持っている場合、最後に言及されたパラメータが優先されます。 関数が呼び出されたときに渡されなかった場合でも。
名前付き関数式
関数定義構文を使用して、関数式を使用して定義された関数に名前を付けることができます。 これは、関数の宣言にはつながりません。したがって、関数の名前はスコープに追加されず、関数の本体とともにスコープの先頭に達することもありません。 ここに私が意味することを説明するためのいくつかの行があります
foo(); // TypeError "foo is not a function" bar(); // baz(); // TypeError "baz is not a function" spam(); // ReferenceError "spam is not defined" var foo = function () {}; // ( 'foo') function bar() {}; // ( 'bar' ) var baz = function spam() {}; // ( 'baz') foo(); // bar(); // baz(); // spam(); // ReferenceError "spam is not defined"
この知識でコードを書く方法
これで、変数と関数宣言のスコープと「発生」を理解できました。 JavaScriptコードを記述するとき、これはどういう意味ですか? 最も重要なことは、常にvarを使用して変数を宣言することです。 スコープごとに正確に1つの変数があり、その先頭にあることを主張します。 あなたがそうすることを強制する場合、「隆起」に関連する問題は決してありません。 ただし、これにより、現在のスコープで宣言されている変数を追跡するのが難しくなります。 強制的にonevarオプションをオンにしてJSLintを使用することをお勧めします。 これを行うと、コードは次のようになります。
/*jslint onevar: true [...] */ function foo(a, b, c) { var x = 1, bar, baz = "something"; }
標準が言うこと
ECMAScript標準に直接アクセスして、すべてがどのように機能するかを理解することは十分に役立ちます。 変数の宣言とスコープについての説明は次のとおりです(セクション12.2.2 )。
関数の宣言内で変数命令が発生した場合、変数は、セクション10.1.3で説明されているように、この関数のローカルスコープ内で宣言されます。 それ以外の場合は、{DontDelete}プロパティ属性を使用して、グローバルスコープで宣言されます(つまり、セクション10.1.3で説明されているようにグローバルオブジェクトのフィールドとして作成されます)。 実行領域に入ると、変数が作成されます。 ブロックは新しい実行領域を定義しません。 プログラムと機能のアナウンスのみが新しいスコープを作成します。 変数は、未定義の値で作成されると初期化されます。 初期化子が定義されている変数には、変数の作成時ではなく、変数命令の実行時に割り当て式の値が割り当てられます。
この記事が、多くの人々が混乱しているJavaScript機能にいくらかの光を当てることを願っています。 あなたをさらに混乱させないように、できる限り一貫性を保とうとしました。 間違いを犯した場合や何かを見逃した場合は、お知らせください。