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