この記事では、JavaScript の最も基本的で重要な概念である実行コンテキストについて詳しく説明します。この記事を読むと、コードを実行する前に JavaScript エンジン内で何が行われるのか、特定の関数や変数が宣言される前に使用できる理由、およびそれらの最終値がどのように定義されるのかが理解できると思います。
Javascript のコードの実行環境は、次の 3 つのタイプに分類されます:
グローバル レベルのコード – これは、コードがロードされると、最初のデフォルトのコード実行環境になります。エンジンが環境に入る事。
関数レベルのコード – 関数が実行されると、関数本体のコードが実行されます。
Eval のコード – Eval 関数内で実行されるコード。
スコープについて説明したリソースはインターネット上にたくさんあります。この記事を誰でも理解しやすくするために、「実行コンテキスト」を現在のコードの実行環境またはスコープと考えることができます。グローバルおよび関数レベルの実行コンテキストを含む以下の例を見てみましょう:
上の図では、合計 4 つの実行コンテキストが使用されています。紫はグローバル コンテキストを表し、緑は person 関数内のコンテキストを表し、青とオレンジは person 関数内の他の 2 つの関数のコンテキストを表します。どのような状況であっても、グローバル コンテキストは 1 つだけ存在し、他のコンテキストからアクセスできることに注意してください。つまり、person のコンテキストでグローバル コンテキストの SayHello 変数にアクセスできます。もちろん、firstName または lastName 関数の変数にもアクセスできます。
関数コンテキストの数に制限はありません。関数が呼び出されて実行されるたびに、エンジンは新しい関数コンテキストを自動的に作成し、それを で使用できます。ローカルスコープ。プライベート変数などを宣言する場合、ローカルスコープ内の要素に外部コンテキストから直接アクセスすることはできません。上記の例では、内部関数は外部コンテキストで宣言された変数にアクセスできますが、その逆はできません。では、その理由は何でしょうか?エンジン内部ではどのように扱われているのでしょうか?
ブラウザでは、JavaScript エンジンは単一のスレッドとして動作します。つまり、ある時点で 1 つのイベントだけが処理のためにアクティブ化され、他のイベントはキューに入れられて処理を待機します。次の図例は、そのようなスタックを説明しています:
JavaScript コード ファイルがブラウザーによってロードされるとき、デフォルトで最初にグローバル実行コンテキストが入力されることはすでにわかっています。グローバル コンテキストで関数が呼び出されて実行されると、プログラム フローは呼び出された関数に入ります。このとき、エンジンは関数の新しい実行コンテキストを作成し、それを実行コンテキスト スタックの先頭にプッシュします。ブラウザは常に、現在スタックの最上位にあるコンテキストを実行します。実行が完了すると、コンテキストはスタックの最上位からポップされ、その下のコンテキストでコードが実行されます。このようにして、スタック内のコンテキストは順番に実行され、グローバル コンテキストに戻るまでスタックからポップされます。次の例を見てください:
(function foo(i) { if (i === 3) { return; } else { foo(++i); } }(0));
上記の foo が宣言された後、() 演算子を介して直接実行されます。関数コードはそれ自体を 3 回呼び出し、そのたびにローカル変数 i が 1 ずつ増加します。 foo 関数がそれ自体で呼び出されるたびに、新しい実行コンテキストが作成されます。コンテキストが実行を完了するたびに、前のコンテキストがスタックからポップされ、再びグローバル コンテキストに戻るまで、前のコンテキストに戻されます。プロセス全体は次のように抽象化されます:
実行コンテキストの抽象概念は次の点に要約できることがわかります:
シングルスレッド
同期実行
唯一1 つのグローバル コンテキスト
関数の実行コンテキストの数に制限はありません
関数が呼び出されるたびに、呼び出し元の関数自体であっても、その関数に対して新しい実行コンテキストが作成されます。
これで、関数が呼び出されるたびに、新しい実行コンテキストが作成されることがわかりました。ただし、JavaScript エンジン内では、コンテキスト作成プロセスは 2 つの段階に分割されます:
確立フェーズ (関数が呼び出されたときに発生しますが、関数本体内の特定のコードを実行する前)
確立変数、関数、引数オブジェクト、パラメータ
スコープチェーンを確立
thisの値を決定
コード実行フェーズ:
変数の割り当て、関数の参照、他のコードを実行
实际上,可以把执行上下文看做一个对象,其下包含了以上3个属性:
(executionContextObj = { variableObject: { /* 函数中的arguments对象, 参数, 内部的变量以及函数声明 */ }, scopeChain: { /* variableObject 以及所有父执行上下文中的variableObject */ }, this: {} }
确切地说,执行上下文对象(上述的executionContextObj)是在函数被调用时,但是在函数体被真正执行以前所创建的。函数被调用时,就是我上述所描述的两个阶段中的第一个阶段 – 建立阶段。这个时刻,引擎会检查函数中的参数,声明的变量以及内部函数,然后基于这些信息建立执行上下文对象(executionContextObj)。在这个阶段,variableObject对象,作用域链,以及this所指向的对象都会被确定。
上述第一个阶段的具体过程如下:
找到当前上下文中的调用函数的代码
在执行被调用的函数体中的代码以前,开始创建执行上下文
进入第一个阶段-建立阶段:
建立variableObject对象:
初始化作用域链
确定上下文中this的指向对象
建立arguments对象,检查当前上下文中的参数,建立该对象下的属性以及属性值
检查当前上下文中的函数声明:
每找到一个函数声明,就在variableObject下面用函数名建立一个属性,属性值就是指向该函数在内存中的地址的一个引用
如果上述函数名已经存在于variableObject下,那么对应的属性值会被新的引用所覆盖。
代码执行阶段:
执行函数体中的代码,一行一行地运行代码,给variableObject中的变量属性赋值。
下面来看个具体的代码示例:
function foo(i) { var a = 'hello'; var b = function privateB() { }; function c() { } } foo(22);
在调用foo(22)的时候,建立阶段如下:
fooExecutionContext = { variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: undefined, b: undefined }, scopeChain: { ... }, this: { ... } }
由此可见,在建立阶段,除了arguments,函数的声明,以及参数被赋予了具体的属性值,其它的变量属性默认的都是undefined。一旦上述建立阶段结束,引擎就会进入代码执行阶段,这个阶段完成后,上述执行上下文对象如下:
fooExecutionContext = { variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: 'hello', b: pointer to function privateB() }, scopeChain: { ... }, this: { ... } }
我们看到,只有在代码执行阶段,变量属性才会被赋予具体的值。
在网上一直看到这样的总结: 在函数中声明的变量以及函数,其作用域提升到函数顶部,换句话说,就是一进入函数体,就可以访问到其中声明的变量以及函数。这是对的,但是知道其中的缘由吗?相信你通过上述的解释应该也有所明白了。不过在这边再分析一下。看下面一段代码:
(function() { console.log(typeof foo); // function pointer console.log(typeof bar); // undefined var foo = 'hello', bar = function() { return 'world'; }; function foo() { return 'hello'; } }());
上述代码定义了一个匿名函数,并且通过()运算符强制理解执行。那么我们知道这个时候就会有个执行上下文被创建,我们看到例子中马上可以访问foo以及bar变量,并且通过typeof输出foo为一个函数引用,bar为undefined。
为什么我们可以在声明foo变量以前就可以访问到foo呢?
因为在上下文的建立阶段,先是处理arguments, 参数,接着是函数的声明,最后是变量的声明。那么,发现foo函数的声明后,就会在variableObject下面建立一个foo属性,其值是一个指向函数的引用。当处理变量声明的时候,发现有var foo的声明,但是variableObject已经具有了foo属性,所以直接跳过。当进入代码执行阶段的时候,就可以通过访问到foo属性了,因为它已经就存在,并且是一个函数引用。
为什么bar是undefined呢?
因为bar是变量的声明,在建立阶段的时候,被赋予的默认的值为undefined。由于它只要在代码执行阶段才会被赋予具体的值,所以,当调用typeof(bar)的时候输出的值为undefined。
好了,到此为止,相信你应该对执行上下文有所理解了,这个执行上下文的概念非常重要,务必好好搞懂之!
以上がJavascript 実行ステートメントのコンテキストに関するディスカッションの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。