摘要:圖片中的作用域鏈,是全局執行環境中的作用域鏈。然后此活動對象被推入作用域鏈的最前端。在最后調用的時候,創建先構建作用域鏈,再創建執行環境,再創建執行環境的時候發現了一個變量標識符。
從圖書館翻過各種JS的書之后,對作用域/執行環境/閉包這些概念有了一個比較清晰的認識。
栗子說明一切 第一個栗子來看一個來自ECMA-262的栗子:
var x = 10; (function foo() { var y = 20; (function bar() { var z = 30; // "x" and "y" are "free variables" // and are found in the next (after // bar"s activation object) object // of the bar"s scope chain console.log(x + y + z); })(); })();
我們可以用下圖展現上面的例子(父變量對象存儲在函數的Scope屬性內)
首先,可以很容易的理解到一個事實:在從控制臺輸出x+y+z的時候,x和y是在bar()函數中的作用域鏈中bar()的活動對象之下找到的。實際上,foo()函數和bar()函數在執行的時候,他們的scope屬性就已經確定了,他們的scope屬性確定為他們外層的變量對象(VO)的集合。從圖中可知,內存結構可能是這樣的:
// foo的scope屬性是global的VO foo.["[[Scope]]"] = { global.["Variable Object"] } // bar的scope屬性是foo的AO和global的VO的集合 bar.["[[Scope]]"] = {foo.["Activation Object"], global.["Variable Object"]}第二個栗子
這個例子來自《高性能Javascript》
// 全局范圍定義 function add(num1, num2) { var sum = num1 + num2; return sum; }
當add()函數創建的時候,它的scope屬性被確定為全局對象的VO,這個全局對象的VO可能包括window/navigator/document之類等等。關系如圖:
這個scope屬性很特別,他是靜態的,在函數創建的時候便能確定。圖片中的作用域鏈,是全局執行環境中的作用域鏈。而在函數執行的時候,書中說道:
每個執行上下文都有自己的作用域鏈,用于解析標識符。當執行上下文被創建的時候,它的作用域鏈初始化為當前運行函數的scope屬性中的對象。這些值按照他們出現在函數中的順序,被復制到執行上下文的作用域鏈中。這個過程一旦完成,一個被稱為“活動對象(AO)”的新對象就為執行上下文創建好了。活動對象作為函數運行時的變量對象,包含了所有的局部變量,命名參數,參數集合以及this。然后此活動對象被推入作用域鏈的最前端。
可以了解到,作用域鏈是個鏈表,是在函數執行的時候才存在的,也就是函數創建執行環境的時候才開始存在的,它先把這個函數的靜態屬性scope屬性中的所有變量對象按照順序復制到作用域鏈(所以這樣就不會擔心作用域鏈嵌套的問題),然后創建AO放在作用域鏈頂部“0號位”。例如再執行代碼:
var total = add(5, 10);
圖片如下圖:
所以,我們也可以得到一個驚人的結論:
函數作用域鏈 = 活動對象(AO) + scope屬性
關鍵的來了這個結論中:活動對象(AO)是臨時的,動態的,獨一無二的。scope屬性是靜態的,確定的。
所以說,函數的作用域鏈,是函數執行的時候動態創建的,但是它又是基于靜態詞法的環境(scope屬性)。所謂“動態創建”,是指在函數執行的時候,先創建之前沒有的作用域鏈,再創建活動對象,然后活動對象推入作用域鏈最前端;所謂“基于靜態的詞法環境”是指函數定義的時候,這個函數本是沒有作用域鏈的,有的只有scope屬性,而這個屬性指向了這個函數外部的執行環境,而這個外部的執行環境擁有作用域鏈(因為這是外部創建外部的執行環境才擁有作用域鏈的,這樣有一點遞歸的味道)。P.S.其實有的版本也說,作用域鏈的確定應該是在活動變量創建完成之后的,這個有待鉆研。
P.S 在ES5規范文檔中,進入函數代碼的流程:
變量提升的本質就是函數在創建執行環境中的變量對象的時候,記錄下了函數聲明,變量和參數等等。具體參見深入理解Javascript之執行上下文(Execution Context),下面是片段:
扯到閉包建立Variable Object對象順序:
建立arguments對象,檢查當前上下文中的參數,建立該對象下的屬性以及屬性值
檢查當前上下文中的函數聲明: 每找到一個函數聲明,就在variableObject下面用函數名建立一個屬性,屬性值就是指向該函數在內存中的地址的一個引用。如果上述函數名已經存在于variableObject下,那么對應的屬性值會被新的引用所覆蓋。
檢查當前上下文中的變量聲明: 每找到一個變量的聲明,就在variableObject下,用變量名建立一個屬性,屬性值為undefined。如果該變量名已經存在于variableObject屬性中,直接跳過(防止指向函數的屬性的值被變量屬性覆蓋為undefined),原屬性值不會被修改。
閉包,在離散數學中指的是滿足性質A的一個最小關系集R,這可以理解這個關系集R,在性質A上封閉。閉包不是一種魔法,雖然可以通過閉包扯得很遠很遠,通過函數的作用域鏈的組成為AO+scope屬性,為快速理解閉包中變量引用來自哪里提供了思路————沒那么復雜,就直接再執行的函數定義處上看就行了。把函數定義的作用域看成是函數執行的作用域。這也是詞法作用域迷人的地方。
Show Me the Code說了那么多,有代碼才是王道,畢竟“Talk is cheap”。
“面向對象”一般的編程:實現封裝這段代碼來自MDN-用閉包模擬私有方法,有更改
var Counter = (function() { var privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment: function(dis) { changeBy(dis); }, decrement: function(dis) { changeBy(-dis); }, value: function() { return privateCounter; } } })(); console.log(Counter.value()); // 0 Counter.increment(1); Counter.increment(2); console.log(Counter.value()); // 3 Counter.decrement(5); console.log(Counter.value()); // -2
返回的是一個對象,這個對象有三個屬性,都是函數。而且這三個函數的scope屬性都是指向一個集合,這個集合包括外層匿名函數的的AO,和全局變量的VO。分析一下Counter.value()這個調用:value這個屬性對應的匿名函數定義的時候,它的scope屬性確定,這個是詞法作用域的特性,這個scope屬性指向的是外部所有變量對象的集合(也就是上句說的那個集合)。在最后調用Counter.value()的時候,創建先構建作用域鏈,再創建執行環境,再創建執行環境的時候發現了一個變量標識符privateCounter。好,接下來在函數體內找找這個對應的值,找不到;到外層的函數,也就是那個Counter對應的匿名函數,誒找到了!好,將這個標識符和這個量“關聯起來”。
結果,這樣下來,返回的這個對象就類似于面向對象變成中的“外部接口”,而沒有被返回的那部分(也就是代碼中的var privateCounter 和function changeBy)則成了“私有的”,無法從外部直接訪問。這樣的閉包模擬了數據的封裝和隱藏,一股熟悉而濃郁的C++味道襲來。當然,這樣用的確不錯,但是關乎性能方面,MDN這樣推薦道:
執行環境到底是怎么建立的?如果不是因為某些特殊任務而需要閉包,在沒有必要的情況下,在其它函數中創建函數是不明智的,因為閉包對腳本性能具有負面影響,包括處理速度和內存消耗。
例如,在創建新的對象或者類時,方法通常應該關聯于對象的原型,而不是定義到對象的構造器中。原因是這將導致每次構造器被調用,方法都會被重新賦值一次(也就是說,為每一個對象的創建)。
下面片段來自深入理解Javascript之執行上下文(Execution Context)
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 }, // 作用鏈和變量對象順序問題,有待鉆研,T.T // 在官方文檔中,貌似是作用域鏈先被創建(而且被稱作詞法環境組件) 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: { ... } }
我們看到,只有在代碼執行階段,變量屬性才會被賦予具體的值。
總結一下分析代碼的時候,務必回看函數的定義,畢竟人家函數是一等貴族。
記住函數作用域鏈 = (動)活動對象(AO) + (靜)scope屬性。
執行環境結構:
執行環境創建后,才開始執行代碼,變量對象才開始被賦值
變量提升 ==> 變量對象的創建
閉包 ===> 作用域鏈中靜態的部分,即scope屬性
官方文檔的補充
我的理解:詞法環境組件 ≈ 作用域;變量環境組件 ≈ 變量對象;
以初始化全局代碼的時候,貌似是創建變量對象在先。(這樣有什么特殊的意義嗎?)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/79161.html
摘要:在之前我們根絕對象的原型說過了的原型鏈,那么同樣的萬物皆對象,函數也同樣存在這么一個鏈式的關系,就是函數的作用域鏈作用域鏈首先先來回顧一下之前講到的原型鏈的尋找機制,就是實例會先從本身開始找,沒有的話會一級一級的網上翻,直到頂端沒有就會報一 在之前我們根絕對象的原型說過了js的原型鏈,那么同樣的js 萬物皆對象,函數也同樣存在這么一個鏈式的關系,就是函數的作用域鏈 作用域鏈 首先先來回...
摘要:而外層的函數不能訪問內層的變量或函數,這樣的層層嵌套就形成了作用域鏈。閉包閉包是指有權訪問另一個函數作用域中的變量的函數,創建閉包的最常見的方式就是在一個函數內創建另一個函數,通過另一個函數訪問這個函數的局部變量。 閉包是js中一個極為NB的武器,但也不折不扣的成了初學者的難點。因為學好閉包就要學好作用域,正確理解作用域鏈,然而想做到這一點就要深入的理解函數,所以我們從函數說起。 函數...
摘要:之前一篇文章我們詳細說明了變量對象,而這里,我們將詳細說明作用域鏈。而的作用域鏈,則同時包含了這三個變量對象,所以的執行上下文可如下表示。下圖展示了閉包的作用域鏈。其中為當前的函數調用棧,為當前正在被執行的函數的作用域鏈,為當前的局部變量。 showImg(https://segmentfault.com/img/remote/1460000008329355);初學JavaScrip...
摘要:在這個情況下我們可能需要使用構造函數,其以指定的模式來創造對象。構造函數也有自己的,值為,也通過其屬性關聯到。從邏輯上來說,這是以棧的形式實現的,它叫作執行上下文棧。 原文:http://dmitrysoshnikov.com/ecmascript/javascript-the-core/ 對象 原型鏈 構造函數 執行上下文棧 執行上下文 變量對象 活動對象 作用域鏈 閉包 Thi...
摘要:函數的作用域會在函數執行時用到,函數每次執行都會創建一個執行環境的內部對象,每個執行環境都有自己的作用域鏈。假設執行,其對應的作用域鏈如下函數執行過程中,變量的查找時從作用域頭部開始查找,如果找到就是使用改變量的值。 每一個函數存在一個[[Scope]]內部屬性,包含了一個函數被創建得作用域中對象得集合,這個集合為函數得作用域鏈。例如下面的全局函數: fucntion add(num1...
閱讀 1588·2021-09-24 10:38
閱讀 1521·2021-09-22 15:15
閱讀 3070·2021-09-09 09:33
閱讀 913·2019-08-30 11:08
閱讀 648·2019-08-30 10:52
閱讀 1261·2019-08-30 10:52
閱讀 2356·2019-08-28 18:01
閱讀 530·2019-08-28 17:55