1はじめに
JavaScriptクロージャは、バックエンドのJavaプログラマーが悩み、理解するのが難しいという概念です。この記事では、jsエンジンに基づいてクロージャの操作原理を分析し、誰もがクロージャを深く理解できるようにします。
2.なぜRhinoエンジンのJavaバージョンの分析に基づいているのですか?
- javaは最高の言語です
- Rhinoエンジンは、有名なjavascriptエンジンspiderMonkeyのJavaバージョンです。
Rhino是jdk 1.6自带的js引擎,出自mozilla,其实现原理与firefox的js引擎高度相似
项目介绍: https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino
源码地址: https://github.com/mozilla/rhino
最新版本的Nashorn为了执行性能,将js转换成为jvm字节码,
不利于我们剖析javascript的真正运行原理
3.例
3.1典型的な閉鎖の例
var fun = (function(){
var count = 101;
function addCount(){
count+=1;
return count;
}
return addCount;
})();
var result = fun();
一部のJavaバックエンドプログラマーは、上記のようなコード構造に少し不安を感じるでしょう。内部の関数には別の関数層があり、最終的に関数を返し、(...)();のような構造があるためです。 。上記を置くことができます例を以下に変更するか、誰もがよく理解できます。
function testFun(){
var count = 101;
function addCount(){
count+=1;
return count;
}
return addCount;
}
var result = testFun()();
上記の例を変更した後は、比較的明確です。testFun()を実行した後、戻り値はaddCount関数であり、別の()を追加すると、addCount()関数が実行されます。
一部のリーダーが()()に慣れていない場合は、上記のコードは最終的に最終バージョンの例として書き直すことができます
3.2最終バージョンの例
function testFun(){
var count = 101;
function addCount(){
count+=1;
return count;
}
return addCount;
}
var funMethod = testFun();
var result = funMethod();
上記の最後の例は、Javaプログラマーよりもはるかに快適です。
しかし、一部のプログラマーはまだ疑問を持っているかもしれません。習慣的な考え方によれば、testFun()関数が戻った後、結果が返された後、そのローカル変数カウントをリサイクルする必要があります。そのため、addCount関数はそれを引き続き使用できます。上記のコード:
var f1 = testFun();
var f2 = testFun();
console.log("闭包调用结果:"+testFun()());
console.log("f1第1次调用结果:"+f1());
console.log("f1第2次调用结果:"+f1());
console.log("f2第1次调用结果:"+f2());
console.log("f2第2次调用结果:"+f2());
結果は次のとおりです。
闭包调用结果:102
f5.html:18 f1第1次调用结果:102
f5.html:19 f1第2次调用结果:103
f5.html:20 f2第1次调用结果:102
f5.html:21 f2第2次调用结果:103
結果を正しく確実に推測できますか?可能であれば、この記事を読む必要はありません。推測が間違っている場合は、この記事を読み続けるのに数分かかることがあります。
4動作原理の詳細説明
4.1スコープオブジェクトスコープ
Javascriptは、c、c ++、javaなどの言語とは異なります。c、c ++、およびjava言語のメソッド呼び出しスタックは、プロセスの事前に割り当てられたスレッドスペースに存在します。メソッドがスタックを呼び出し続けると、プロセススペースの各スレッドのスタックスペースが上位アドレスの場所に割り当てられ、ヒープスペースが下位アドレスに割り当てられるため、spの値はますます小さくなっています。スペース)、メソッドが終了すると、スタックはますます浅くなります。
ただし、javascriptは解釈される言語であり、そのスタック構造はc、c ++、javaとは大きく異なり
ます。javascriptの各変数は、指定されたスコープに格納する必要があります。グローバルスコープの場合、各関数を実行すると、スタックとスコープが作成されます。オブジェクトスコープ、関数が複数回実行され、複数のスコープが作成されます。
この最終バージョンの例のスコープを次の図に示します。
4.2 var funMethod = testFun()は何をしましたか?
表面的には、このコード行は最初にtestFun()メソッドを実行し、最後にaddCount関数を返します。
次の図に示すように。
4.2.1testFun関数の実行
- testFun関数を実行すると、その関数に対応するスコープオブジェクトがデフォルトで作成されます
本函数中的局部变量及函数中定义的函数都会记录在这个scope中
- addCount関数オブジェクトを返します
返回的addCount函数对象的parentScope会指向
testFun函数所创建的scope对象
4.2.2addCount関数オブジェクトをfunMethodに割り当てます
最終的に、addCount関数がfunMethod変数に割り当てられ、testFun関数
の呼び出しスタックが破棄されます。
ただし、testFun関数の実行中に作成されたスコープは、addCount関数のparentScopeによって参照されるため、破壊される運命を逃れる
4.3 var result = funMethod()
ここでのfunMethodの実行は、実際にはfunMethod自体が指すaddCount関数オブジェクトを実行することです。addCountが実行されると、新しいスコープは作成されませんが、親スコープのtestFunのようなスコープが使用されるため、count変数は次のようになります。通常使用
5コード実行プロセス分析
5.1javascriptの実行フロー
5.1.1字句解析/トークン化
このプロセスは、文字列をトークンに分解します。
5.1.2構文解析/構文解析
語彙単位メソッドを抽象構文ツリーに変換します(抽象構文ツリーAST)
5.1.3バイトコードコードの生成
ast構文木からIRコードを生成し、最後にbyteCodeコードを生成します
5.1.4インタープリターモードでbyteCodeを実行する
解釈された方法でbyteCodeコードを実行する
5.2ケース分析
最終バージョンの例を使用して、クロージャの動作原理を示します
5.2.3メインコードブロックのbyteCode
ICode dump, for null, length = 25
MaxStack = 3
[0] REG_IND_C0
[1] CLOSURE_EXPR org.mozilla.javascript.InterpreterData@4617c264
[2] POP_RESULT
[3] LINE : 1
[6] REG_STR_C0
[7] BINDNAME
[8] REG_STR_C1
[9] NAME_AND_THIS
[10] REG_IND_C0
[11] CALL 0
[12] REG_STR_C0
[13] SETNAME
[14] POP
[15] REG_STR_C2
[16] BINDNAME
[17] REG_STR_C0
[18] NAME_AND_THIS
[19] REG_IND_C0
[20] CALL 0
[21] REG_STR_C2
[22] SETNAME
[23] POP
[24] RETURN_RESULT
PCのシリアル番号は左側の括弧内にあります。
5.2.3.1スタックを作成する
このバイトコードを実行する前に、スタックオブジェクトCallFrameを作成する必要があります。CallFrame.initializeArgsを渡し
、CallFrameスタックを初期化します。
5.2.3.2スタックを初期化する
- このコードセグメントでは、スコープはグローバルスコープを使用して、
ScriptRuntime.initScript(...)を介して現在のスコープ(グローバル)にresultやfunMethodなどのローカル変数を格納します。 - コードスニペットのオブジェクトのitsNestedFunctionsプロパティに従って、コードスニペットに関数が含まれているかどうかを確認します。関数を初期化するためにinitFunction()メソッドが呼び出され、関数オブジェクトが現在のスコープに格納されている場合。この例では、最も外側のコードスニペットには、testFun関数が含まれています
initFunction方法初始化函数时,会设置本函数的父scope,
将上层的prototype赋给本函数的prototype
- スタックスペースを割り当てる
5.2.3.3メインバイトコード実行の説明
すべての命令は、Interpreter.interpretLoop()によって解釈および実行されます。
[1] CLOSURE_EXPR
testFun関数オブジェクトをstack [3]に格納します
[2] POP_RESULT
frame.result = stack [3] // testFun関数オブジェクトをframe.resultに格納します
[7] BINDNAME
funMethodローカル変数が配置されているスコープを見つけて、スタックに配置します[3]
[9] NAME_AND_THIS
現在のスコープでtestFunオブジェクトを見つけて、stack [4]に
配置し、testFunのようなスコープ(つまり、グローバルスコープ)をstack [5]に配置します。
[11] CALL 0
インスタントスタック[4](つまりtestFunオブジェクト)はfunオブジェクトに格納されます
インスタントスタック[5](つまりtestFunオブジェクト)はfunThisObjオブジェクトに格納されます
testFun関数を初期化して新しいCallFrame
を実行し、最後にtestFun関数を実行します。
詳細を確認するtestFunスタック以下のtestFun関数のbyteCodeの説明
[13] SETNAME
stack [4]からaddCount関数オブジェクトを取得し、現在のコードブロックスコープのfunMethodのローカル変数に格納します
[18] NAME_AND_THIS
stack [4] addCount関数オブジェクトを保存します
stack [5]現在のスコープ(つまりグローバルスコープ)を保存します
[20] CALL 0
addCount関数の呼び出しと実行
funオブジェクトはaddCount関数です
funThisObjはグローバルスコープです。
以下を参照してください。addCount関数スタックの初期化
[22] SETNAME
102を現在のスコープの結果ローカル変数に設定します
5.2.4testFun関数のbyteCode
ICode dump, for testFun, length = 14
MaxStack = 2
[0] LINE : 1
[3] REG_STR_C0
[4] BINDNAME
[5] SHORTNUMBER 101
[8] REG_STR_C0
[9] SETNAME
[10] POP
[11] REG_STR_C1
[12] NAME
[13] RETURN
5.2.4.1testFun関数スタックを初期化する
- testFun関数のスコープ(クラス名はNativeCall)を作成し、その親スコープをこのスコープに設定し、デフォルト変数(引数、前のカウント)を追加します。
- この関数で新しい関数addCountが定義されていることを確認し、初期化して、このスコープに記録します
5.2.4.2testFunバイトコード実行の説明
[4]バインド名
count変数を含むスコープを確認し、このスコープを返し、stack [2]に格納します。
[5]ショートナンバー101
stack[3] = DBL_MRK;//标识栈这个位置为double
sDbl[3] = getShort(iCode, frame.pc);//即101,sDbl是栈中存数值的地方
[9] SETNAME
testFun関数のスコープのカウントのstack [3]に値を格納します
名前
testFun関数からaddCount関数を取得し、stack [2]に格納します。
返品
frame.result = stack [2]
はaddCountを返します
addCount関数の5.2.5バイトコード
ICode dump, for addCount, length = 15
MaxStack = 3
[0] LINE : 1
[3] REG_STR_C0
[4] BINDNAME
[5] REG_STR_C0
[6] NAME
[7] ONE
[8] ADD
[9] REG_STR_C0
[10] SETNAME
[11] POP
[12] REG_STR_C0
[13] NAME
[14] RETURN
5.2.5.1addCount関数スタックを初期化する
- ここでのスコープは、testFun関数のスコープを指します
5.2.5.2addCountバイトコード実行の説明
[6]名前
現在のスコープからcount変数の値を見つけて、stack [1]に格納します。
[7] ONE
stack[2] = DBL_MRK;
sDbl[2] = 1;
[8]追加
sDbl [1]の値が102に追加されます
[10] SETNAME
スコープのカウントにsDbl [1]の値を格納します
[13]名前
カウントをstack [0]のスコープに格納します
[14]リターン
frame.result = stack[0];
frame.resultDbl = sDbl[0];
著者:ウーリアンティアン