ホームページ  >  記事  >  ウェブフロントエンド  >  JavaScript の字句スコープとクロージャー分析の説明_JavaScript スキル

JavaScript の字句スコープとクロージャー分析の説明_JavaScript スキル

WBOY
WBOYオリジナル
2016-05-16 18:21:281024ブラウズ
コードをコピーします コードは次のとおりです:

var classA = function(){
this。 prop1 = 1 ;
}
classA.prototype.func1 = function(){
var that = this,
var1 = 2;

function a(){
return function( ){
alert(var1);
a();
}; 🎜>var objA = new ClassA();
objA.func1();


実際、ここで表現したいのは、次のようなことです。メソッドが定義されている メソッドが実行される場所からは数千マイル離れていますが、どの変数にアクセスでき、どの変数にアクセスできないのかを判断するにはどうすればよいでしょうか。これが今回分析する必要がある問題です - 字句スコープ

字句スコープ: 変数のスコープは、変数の実行時ではなく定義時に決定されます。つまり、字句スコープは次のものに依存します。ソース コードは静的分析を通じて決定できるため、字句スコープは静的スコープとも呼ばれます。 withとevalを除けば、JSのスコープ機構は字句スコープ(Lexicalscope)に非常に近いとしか言​​えません。


いくつかの小さなケースを通じて、語彙のスコープとクロージャーを理解するために不可欠な、JS 実行中のいくつかの基礎となる概念と理論的知識を深く理解し始めます。

古典的なケースの再登場

1. 古典的なケース 1



コードをコピー
コードは次のとおりです。 /*グローバル (ウィンドウ) ドメイン内のコード*/ function a(i) {
var i;
alert( i);
};
a(10);


質問: 上記のコードは何を出力しますか?
答え: はい、10 個が表示されます。具体的な実行処理は次のようになります。
a 関数には仮引数 i があり、a 関数を呼び出すときに実引数 10 が渡され、仮引数 i=10
次にローカル同じ名前の変数 i が定義されていますが、値が割り当てられていません。
アラート出力 10
考察: ローカル変数 i と仮パラメータ i は同じ記憶領域ですか?

2. 古典的なケース 2



コードをコピーします
コードは次のとおりです: /*グローバル (ウィンドウ) ドメイン内のコード*/ function a(i) {
alert(i);
alert(arguments[0]); /arguments[0] は、仮パラメータ i
var i = 2
alert(arguments[0]);
a(10); );


質問: 上記のコードは何を出力しますか? ((10,10,2,10 10,10,2,2))
答え: FireBug での実行結果は 2 番目の 10,10,2,2 です。ご想像の通り...、簡単に説明しましょう。 it 特定の実行プロセス

a 関数には仮パラメータ i があり、a 関数を呼び出すとき、実パラメータ 10 が渡され、仮パラメータ i=10
最初のアラートは値 10 を出力します。仮パラメータ i
の 2 番目のアラートは、引数 [0] を出力します。これも i
である必要があります。次に、ローカル変数 i を定義し、値 2 を割り当てます。このとき、ローカル変数 i=2
3 番目のアラートは、ローカル変数 i の値を 2 に設定します。
4 番目のアラートは、arguments[0] を再度出力します。
考え: これは、ローカル変数 i と仮パラメータ i の値が次のとおりであることを説明できますか?同じ?

3. 古典的なケース 3




コードをコピーします
コードは次のとおりです: /*グローバル (ウィンドウ) ドメイン内のコード*/ function a(i) { var i = i; alert(i); ;
a( 10);


質問: 上記のコードは何を出力しますか? (( unknown 10 ))
答え: FireBug での実行結果は 10 です。具体的な実行プロセスについて簡単に説明します。

最初の文では、仮パラメータ i と同じ名前のローカル変数 i を宣言しています。結果によれば、後者の i は
仮パラメータ i を指していることがわかります。したがって、ここでは、仮パラメータ i の値 10 をローカル変数 i に代入することと同等です。
2 番目のアラートは当然のことながら、出力 10
思考: 組み合わせケース 列 2、これは基本的に、ローカル変数 i と仮パラメータ i が同じストレージ アドレスを指していることを示しています。

4. 古典的なケース 4





コードをコピーします
コードは次のとおりです:
;
疑问:上面的代码又会输出什么呢?(小子,看这回整不死你!哇哈哈,就不给你选项)
答案:在FireBug中的运行结果是 undefined, 2,下面简单说一下具体执行过程

第一个alert输出undefined
第二个alert输出 2
思考:到底怎么回事儿?
5、经典案例五…………..N
看到上面的几个例子,你可能会想,怎么可能,我写了几年的 js 了,怎么这么简单例子也会犹豫,结果可能还答错了。其实可能原因是:我们能很快的写出一个方法,但到底方法内部是怎么执行的呢?执行的细节又是怎么样的呢?你可能没有进行过深入的学习和了解。要了解这些细节,那就需要了解 JS 引擎的工作方式,所以下面我们就把 JS 引擎对一个方法的解析过程进行一个稍微深入一些的介绍

解析过程

1、执行顺序

  • 编译型语言,编译步骤分为:词法分析、语法分析、语义检查、代码优化和字节生成。
  • 解释型语言,通过词法分析和语法分析得到语法分析树后,就可以开始解释执行了。这里是一个简单原始的关于解析过程的原理,仅作为参考,详细的解析过程(各种JS引擎还有不同)还需要更深一步的研究

JavaScript执行过程,如果一个文档流中包含多个script代码段(用script标签分隔的js代码或引入的js文件),它们的运行顺序是:

  1. 步骤1. 读入第一个代码段(js执行引擎并非一行一行地执行程序,而是一段一段地分析执行的)
  2. 步骤2. 做词法分析和语法分析,有错则报语法错误(比如括号不匹配等),并跳转到步骤5
  3. 步骤3. 对【var】变量和【function】定义做“预解析“(永远不会报错的,因为只解析正确的声明)
  4. 步骤4. 执行代码段,有错则报错(比如变量未定义)
  5. 步骤5. 如果还有下一个代码段,则读入下一个代码段,重复步骤2
  6. 步骤6. 结束

2、特殊说明
全局域(window)域下所有JS代码可以被看成是一个“匿名方法“,它会被自动执行,而此“匿名方法“内的其它方法则是在被显示调用的时候才被执行
3、关键步骤
上面的过程,我们主要是分成两个阶段

  1. 解析:就是通过语法分析和预解析构造合法的语法分析树。
  2. 执行:执行具体的某个function,JS引擎在执行每个函数实例时,都会创建一个执行环境(ExecutionContext)和活动对象(activeObject)(它们属于宿主对象,与函数实例的生命周期保持一致)

3、关键概念
到这里,我们再更强调以下一些概念,这些概念都会在下面用一个一个的实体来表示,便于大家理解

  • 语法分析树(SyntaxTree)可以直观地表示出这段代码的相关信息,具体的实现就是JS引擎创建了一些表,用来记录每个方法内的变量集(variables),方法集(functions)和作用域(scope)等
  • 执行环境(ExecutionContext)可理解为一个记录当前执行的方法【外部描述信息】的对象,记录所执行方法的类型,名称,参数和活动对象(activeObject)
  • 活动对象(activeObject)可理解为一个记录当前执行的方法【内部执行信息】的对象,记录内部变量集(variables)、内嵌函数集(functions)、实参(arguments)、作用域链(scopeChain)等执行所需信息,其中内部变量集(variables)、内嵌函数集(functions)是直接从第一步建立的语法分析树复制过来的
  • 词法作用域:变量的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域。 with和eval除外,所以只能说JS的作用域机制非常接近词法作用域(Lexical scope)
  • 作用域链:词法作用域的实现机制就是作用域链(scopeChain)。作用域链是一套按名称查找(Name Lookup)的机制,首先在当前执行环境的 ActiveObject 中寻找,没找到,则顺着作用域链到父 ActiveObject 中寻找,一直找到全局调用对象(Global Object)

4、实体表示
源码,语法分析树,执行环境和活动对象的引用关系

解析シミュレーション

これを見ても、誰もがまだ混乱していると思います。解析ツリーとは何なのか、解析ツリーはどのようなものなのか、スコープ チェーンはどのように実装されているのか、アクティブ オブジェクトの内容は何なのかなど。あまり明確ではありません。以下では、実際のコードを使用して解析プロセス全体をシミュレーションします。実際に構文解析ツリーとアクティブ オブジェクトを作成し、スコープとスコープ チェーンの実装方法を理解します。

1シミュレーション コード
コードをコピー コードは次のとおりです:

/* グローバル ( window) ドメイン コードの一部*/
var i = 1,j = 2,k = 3;
function a(o,p,x,q){
var x = 4; >alert(i );
function b(r,s) {
var i = 11,y = 5;
function c(t){
var z = 6;
alert(i);
//関数式
var d = function(){
alert(y); c(60);
b(40,50);

; 🎜>2 , 解析ツリー
上記のコードは非常に単純です。最初にいくつかのグローバル変数とグローバル メソッドを定義し、次にメソッド内でローカル変数とローカル メソッドを定義します。ここで、JS インタープリターがこのコードを読み取り、解析を開始します。前述したように、JS エンジンはまず構文解析と事前解析を通じて構文解析ツリーを取得します。構文解析ツリーがどのようなもので、どのような情報が含まれているかについては、以下では単純な構造である JS オブジェクトを使用します (わかりやすくするため)。さまざまなオブジェクトを表します (ここにあるものは単なる擬似オブジェクト表現であり、実行できない場合があります) 構文解析ツリーを記述します (これは私たちがよく知っているものです。実際の構造については掘り下げません。実際の構造はさらに複雑になるはずです)これは、特に解析プロセスを理解するのに役立ちます。




コードをコピーします。

コードは次のとおりです。 >
/**
* 構文解析ツリーの構築をシミュレートし、変数とメソッドを関数に格納します
*/
var SyntaxTree = { // 構文解析ツリー内のグローバル オブジェクトの表現 window: { variables:{ i: { 値:1}、j:{ 値: 2}、k:{ 値:3}
}、
関数:{
a: this.a
}
}、

a:{
変数:{
x:"未定義"
}、
関数:{
b: this.b
},
スコープ: this.window
} ,

b:{
変数:{
y:"未定義"
},
関数:{
c: this.c、d: this .d
}、
スコープ: this.a
}、

c:{
変数:{
z:"未定義"
}、
関数:{}、
スコープ: this.b
}、

d:{
変数:{}、
関数:{},
スコープ: {
myname:d,
スコープ: this.b
}
}
}; >上記は解析ツリーを簡単に表現したものです 先ほど分析したように、解析ツリーは主に各関数の変数セット(変数)、メソッドセット(関数)、スコープ(スコープ)を記録します
解析ツリーのキーポイント解析ツリー

1 変数セット (変数)、変数定義のみがあり、変数値はありません。このとき、変数値はすべて「未定義」です。
2 スコープ (スコープ)。レキシカルスコープの特性により、各変数のスコープはこの時点ですでに明確であり、実行環境によって変更されることはありません。 【それはどういう意味ですか?つまり、メソッドを返し、それを別のメソッドで実行することがよくあります。実行時には、メソッド内の変数のスコープは、メソッドが定義されたときのスコープに基づきます。実際、ここで私が言いたいのは、メソッドを実行する場所がどれほど複雑であっても、メソッド内の変数にアクセスできるかどうかの最終判断は、メソッドが定義された場所に戻って検証する必要があるということです。 ]
3 スコープ (スコープ) ルールの設定
a 関数宣言および匿名関数式の場合、[スコープ] は作成時のスコープです
b 名前付き関数式の場合、[スコープ] の先頭は a新しい JS オブジェクト (つまり、Object.prototype を継承します)。このオブジェクトには 2 つのプロパティがあります。1 つ目はそれ自体の名前で、2 つ目は関数内のコードがアクセスできるようにするために定義された関数名です。関数名自体をエラーなしで再帰的に実行します。
3. 実行環境とアクティブなオブジェクト
構文解析が完了し、コードの実行が開始されます。各メソッドを呼び出すと、JS エンジンはメソッド インスタンスのライフ サイクルと一致する実行環境とアクティブ オブジェクトを自動的に確立し、上記のメソッドの実行に必要な実行サポートを提供します。次のように、アクティブ オブジェクトが均一に確立されます (理論的には、アクティブ オブジェクトはメソッドの実行時に生成されます。デモンストレーションの便宜上、ここではすべてのメソッドのアクティブ オブジェクトを一度に定義します):
実行環境



コードをコピー


コードは次のとおりです:

/**
* 実行環境: 関数の実行時に作成される実行環境
*/
var ExecutionContext = {
window: {
type: "global",
name: "global",
body: ActiveObject.window
},

a:{
type: "function",
name: "a",
body: ActiveObject.a,
scopeChain: this .window.body
},

b:{
type: "function",
name: "b",
body: ActiveObject.b,
scopeChain: this.a.body
},

c:{
type: "function",
name: "c",
body: ActiveObject.c,
scopeChain : this.b.body
},

d:{
type: "function",
name: "d",
body: ActiveObject.d,
scopeChain: this.b.body
}
}

上記の各メソッドの実行環境には、対応するメソッドの種類 (関数)、メソッド名 (funcName)、およびアクティブなメソッドが格納されます。オブジェクト (ActiveObject)、スコープ チェーン (scopeChain) およびその他の情報の重要なポイントは次のとおりです:

body 属性、現在のメソッドのアクティブ オブジェクトを直接指します
scopeChain 属性、スコープ チェーン、はリンクされたリスト構造です。解析ツリー内の現在のメソッドに対応するスコープ属性に従って、スコープに対応するメソッドのアクティブなオブジェクト (ActivceObject) を指します。変数検索は、このチェーンに従って アクティブオブジェクト

コードをコピー コードは次のとおりです:
/**
* アクティブ オブジェクト: 関数の実行時に作成されるアクティブ オブジェクトのリスト
*/
var ActiveObject = {
window: {
variables:{
i: { value:1},
j: { value:2},
k: {値:3}
}、
関数:{
a: this.a
}
}、

a:{
変数:{
x: {値:4}
}、
関数:{
b: SyntaxTree.b
}、
パラメータ:{
o: {値: 10}、
p: {値: 20}、
x: this.variables. x、
q: "未定義"
}、
引数:[this.parameters.o,this.parameters.p ,this.parameters.x]
},

b:{
変数:{
y:{ 値:5}
},
関数:{
c: SyntaxTree.c,
d: SyntaxTree.d
},
parameters:{
r:{value:40},
s:{value:50}
},
引数:[this.parameters.r,this.parameters.s]
},

c:{
変数:{
z:{ 値:6}
}、
関数:{}、
パラメータ: {
u:{value:70}
}、
引数:[this.parameters.u]
} 、

d:{
変数:{}、
関数:{}、
パラメータ:{}、
引数:[]
}
}

上記の各アクティブオブジェクトには、対応するメソッドが格納されており、内部変数セット(変数)、組み込み関数セット(関数)、仮パラメータ(パラメータ)、実パラメータ(引数)など実行に必要な情報が格納されています。アクティブオブジェクトの要点

構文解析からアクティブオブジェクトを作成 ツリーコピーメソッドの内部変数セット(変数)と埋込み関数セット(関数)
メソッドの実行を開始し、アクティブなオブジェクトに設定されている内部変数はすべて未定義にリセットされます
仮パラメータ(パラメータ)と実パラメータ(引数)のオブジェクトを作成、同名の実パラメータ、仮パラメータと変数は[参照]関係です
実行メソッド内の代入ステートメントを使用すると、変数セット内の変数に値が代入されます。
変数の検索ルールは、まず現在の実行環境の ActiveObject 内を検索します。見つからない場合は、次に指す ActiveObject 内を検索します。実行環境のScopeChainプロパティにより、グローバルオブジェクト(ウィンドウ)の
メソッドが実行されるまで、内部変数の値はリセットされません。変数が破棄されるタイミングについては、以下の
の寿命を参照してください。メソッド内の変数のサイクルは、メソッド インスタンスへのアクティブな参照があるかどうかによって異なります。そうでない場合、アクティブなオブジェクトは破棄されます。
6 と 7 により、クロージャーが外部変数にアクセスできるようになります。この処理の根本的な原因は次のとおりです。同じ名前の変数と仮パラメータは同じメモリ アドレスを参照するため、2 番目のケースで変更された引数がローカル変数に影響を与える状況が発生します (ケース 4

) [JS エンジンの変数検索ルールに従って、最初に現在の実行環境の ActiveObject を検索します (見つからない場合)。次に、実行環境のプロパティ ScopeChain が指す ActiveObject に沿って、グローバル オブジェクト (ウィンドウ) まで検索します。 ], ということで、4番目は、現在の変数 i の定義は ActiveObject で見つかりましたが、値が「未定義」だったので、「未定義」を直接出力しました

まとめ
以上ですJS をより深く理解し、その応用をよりよく理解するために、クロージャを学習する過程で、語彙範囲をある程度理解して要約するために、一定期間 JS を学習して使用しました。これらと実際の JS 解釈エンジンとのいくつかの違いについて、私はシステム設計者ではなく、新しいフロントエンド開発者の観点からこの問題を分析しているだけなので、JS 開発者がこの問題の範囲を理解するのに役立つことを願っています。方法!
声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。