javascriptクロージャの原理を分析します--javaバージョンのjsエンジンと組み合わせて説明します

1はじめに

JavaScriptクロージャは、バックエンドのJavaプログラマーが悩み、理解するのが難しいという概念です。この記事では、jsエンジンに基づいてクロージャの操作原理を分析し、誰もがクロージャを深く理解できるようにします。

2.なぜRhinoエンジンのJavaバージョンの分析に基づいているのですか?

  1. javaは最高の言語です
  2. 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関数の実行

  1. testFun関数を実行すると、その関数に対応するスコープオブジェクトがデフォルトで作成されます
本函数中的局部变量及函数中定义的函数都会记录在这个scope中
  1. 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スタックを初期化する

  1. このコードセグメントでは、スコープはグローバルスコープを使用して、
    ScriptRuntime.initScript(...)を介して現在のスコープ(グローバル)にresultやfunMethodなどのローカル変数格納します。
  2. コードスニペットのオブジェクトのitsNestedFunctionsプロパティに従って、コードスニペットに関数が含まれているかどうかを確認します。関数を初期化するためにinitFunction()メソッドが呼び出され、関数オブジェクトが現在のスコープに格納されている場合。この例では、最も外側のコードスニペットには、testFun関数が含まれています
initFunction方法初始化函数时,会设置本函数的父scope,
将上层的prototype赋给本函数的prototype
  1. スタックスペースを割り当てる

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関数スタックを初期化する

  1. testFun関数のスコープ(クラス名はNativeCall)を作成し、その親スコープをこのスコープに設定し、デフォルト変数(引数、前のカウント)を追加します。
  2. この関数で新しい関数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関数スタックを初期化する

  1. ここでのスコープは、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];

著者:ウーリアンティアン

おすすめ

転載: blog.csdn.net/vipshop_fin_dev/article/details/109272347