摘要:至此作用域鏈創(chuàng)建完畢。好了,通過(guò)深入理解作用域鏈,我們能跟好的理解的運(yùn)行機(jī)制和閉包的原理。
前言
理解javascript中的作用域和作用域鏈對(duì)我們理解js這們語(yǔ)言。這次想深入的聊下關(guān)于js執(zhí)行的內(nèi)部機(jī)制,
主要討論下,作用域,作用域鏈,閉包的概念。為了更好的理解這些東西,我模擬了當(dāng)一個(gè)函數(shù)執(zhí)行時(shí),js引擎做了哪些事情--那些我們看不見(jiàn)的動(dòng)作。
關(guān)鍵詞:
執(zhí)行環(huán)境
作用域
作用域鏈
變量對(duì)象
活動(dòng)對(duì)象
閉包
垃圾回收
執(zhí)行環(huán)境與作用域鏈我們都知道js的執(zhí)行環(huán)境最外層是一個(gè)全局環(huán)境Global,在web瀏覽器的宿主環(huán)境下,window對(duì)象被認(rèn)為是全局執(zhí)行環(huán)境。在后臺(tái)的nodejs環(huán)境global作為全局變量也是我們可以直接訪問(wèn)到的。
某個(gè)執(zhí)行環(huán)境中所有代碼執(zhí)行完畢后,該環(huán)境被銷(xiāo)毀,保存在其中的所有變量和函數(shù)定義也隨之銷(xiāo)毀(全局環(huán)境到應(yīng)用退出--如關(guān)閉網(wǎng)頁(yè)或?yàn)g覽器)
每個(gè)函數(shù)也有自己的執(zhí)行環(huán)境,當(dāng)執(zhí)行流進(jìn)入函數(shù)時(shí),函數(shù)的環(huán)境被推入一個(gè)環(huán)境棧中,函數(shù)執(zhí)行完畢之后,棧將其環(huán)境彈出,把控制權(quán)返回給之前的執(zhí)行環(huán)境。
當(dāng)代碼在一個(gè)環(huán)境中執(zhí)行時(shí),會(huì)創(chuàng)建創(chuàng)建變量對(duì)象的一個(gè)作用域鏈。
如果環(huán)境是個(gè)函數(shù),則將其活動(dòng)對(duì)象作為變量對(duì)象。活動(dòng)對(duì)象在最開(kāi)始只包含一個(gè)變量,即arguments對(duì)象,作用域鏈的下一個(gè)變量對(duì)象來(lái)自下一個(gè)包含環(huán)境,一直延續(xù)到全局環(huán)境。
下面我們模擬下這個(gè)過(guò)程。
var name = "eric"; function say(){ var name = "xu"; console.log(name); } say();//xu
輸出“xu”,而不是“eric”,這個(gè)我們也許都很好理解,因?yàn)楹瘮?shù)內(nèi)部定義了局部同名變量name,而不會(huì)使用全局的name。上面的環(huán)境中包含全局變量name和say函數(shù);當(dāng)say執(zhí)行時(shí),js引擎做了些什么。下面我們模擬下引擎“偷偷”為我們做的事。
作用域鏈的產(chǎn)生過(guò)程首先say()執(zhí)行時(shí)會(huì)創(chuàng)建一個(gè)執(zhí)行環(huán)境,為了形象一些,我這里以三個(gè)大括號(hào)可視化表示一個(gè)執(zhí)行環(huán)境。如:say(){{{...}}}
這個(gè)執(zhí)行環(huán)境中會(huì)自動(dòng)擁有一個(gè)特殊的內(nèi)部屬性[[Scope]](為了更好的理解,可以把它想象成如果是全局環(huán)境的window,全局環(huán)境定義的變量和函數(shù)附著在這個(gè)變量上自動(dòng)成為window的屬性和方法,這樣的一個(gè)局部功能“局部?jī)?nèi)全局對(duì)象”。但其實(shí)局部的變量和函數(shù)會(huì)被附著在其活動(dòng)對(duì)象上,活動(dòng)對(duì)象又是作用域鏈第一個(gè)變量對(duì)象。)
函數(shù)調(diào)用時(shí)與執(zhí)行環(huán)境同時(shí)創(chuàng)建的就是相應(yīng)的作用域鏈[[Scope Chain]],并賦值給特殊變量Scope;
//step 1:創(chuàng)建執(zhí)行環(huán)境,為了形象一些,我這里以三個(gè)大括號(hào)可視化表示一個(gè)執(zhí)行環(huán)境 {{{...}}}
//step 2:創(chuàng)建作用域鏈,并賦值給特殊變量Scope,我們用數(shù)組來(lái)模擬這個(gè)作用域鏈,隨后我會(huì)解釋為什么用數(shù)組模擬 var ScopeChain = [ FirstVariableObject,//函數(shù)內(nèi)的變量對(duì)象 SecondVariableObject //包含這個(gè)函數(shù)的外面一層的變量對(duì)象,在上面的例子中已經(jīng)是全局環(huán)境了。 ] Scope = ScopeChain;
在作用域鏈生成之前,其實(shí)還有步驟,那就是作用域鏈數(shù)組的兩個(gè)變量對(duì)象的生成。那這兩個(gè)變量對(duì)象是什么呢?
其實(shí)第一個(gè)變量對(duì)象就是函數(shù)的活動(dòng)對(duì)象【activation object】,這個(gè)活動(dòng)對(duì)象可以理解成這樣一個(gè)對(duì)象
ActivationObject = { arguments: [] //活動(dòng)對(duì)象最開(kāi)始僅包含arguments(就是函數(shù)內(nèi)隱藏的arguments) }
然后內(nèi)部this根據(jù)環(huán)境,加入活動(dòng)對(duì)象
ActivationObject = { arguments: [], //活動(dòng)對(duì)象最開(kāi)始僅包含arguments(就是函數(shù)內(nèi)隱藏的arguments) this: window //這里的this根據(jù)執(zhí)行環(huán)境和調(diào)用對(duì)象的不同,會(huì)動(dòng)態(tài)變化,上面的例子因?yàn)槭侨汁h(huán)境執(zhí)行的所以this指向window }
然后開(kāi)始尋找var的變量定義,或者函數(shù)聲明(我們都知道的函數(shù)聲明會(huì)被提升)。
此時(shí)的活動(dòng)對(duì)象變成:
//活動(dòng)對(duì)象,即函數(shù)內(nèi)部所有變量的綜合,會(huì)自動(dòng)成為第一個(gè)變量對(duì)象 ActivationObject = { arguments: [], this: window, name: undefined //注意引擎此時(shí)并不會(huì)初始化賦值,只有讀到賦值那一行時(shí)才會(huì)賦值 }
這樣我們就能很好的理解我們熟悉的經(jīng)典例子,為什么下面的console.log不會(huì)報(bào)錯(cuò),也不是輸出"xu",而是undefined
因?yàn)槲覀兊幕顒?dòng)對(duì)象會(huì)自動(dòng)變?yōu)榈谝粋€(gè)活動(dòng)對(duì)象,所以第一個(gè)變量對(duì)象就等于活動(dòng)對(duì)象
FirstVariableObject = ActivationObject;
同理作用域中的第二個(gè)變量對(duì)象SecondVariableObject,或者我們也可以命名為GlobalVariableObject,因?yàn)樵谏厦娴睦又幸呀?jīng)是全局環(huán)境了
//作用域鏈的第二個(gè),也是最后一個(gè)(全局變量對(duì)象) SecondVariableObject = { this: window, say: function (){...}, name: "eric" }
第二個(gè)變量對(duì)象不包含arguments,因?yàn)樗侨汁h(huán)境,而不是函數(shù)。say函數(shù)聲明被提升作為window的全局方法,還有全局的name屬性。都被掛在第二層的作用域鏈的變量對(duì)象上。
至此作用域鏈創(chuàng)建完畢。作用域鏈會(huì)成為這樣的好理解的樣子:
//形象的作用域鏈 Scope = ScopeChain = [ { arguments: [], this: window, name: undefined }, { this: window, say: function (){...}, name: "eric" } ]作用域鏈查找在js執(zhí)行過(guò)程中的模擬
然后js開(kāi)始一句一句解析say函數(shù)的代碼,
第一句,var name = "xu"
此時(shí),活動(dòng)對(duì)象的name值才會(huì)將undefined變?yōu)?xu";
然后執(zhí)行第二句console.log(name);
這句中有一個(gè)變量name,這個(gè)時(shí)候作用域鏈就該出場(chǎng)了。
js引擎會(huì)開(kāi)始執(zhí)行查找,首先從ActivationObject活動(dòng)對(duì)象中開(kāi)始找,因?yàn)榻?jīng)過(guò)var name = "eric";
此時(shí)作用域鏈的第一個(gè),即活動(dòng)對(duì)象已經(jīng)變成
{ arguments: [], this: window, name: "xu" }
所以輸出‘xu’,而不是‘eric’
如果我們將say函數(shù),做下改動(dòng)如下:
var name = "eric"; function say(){ var age = 99; console.log(name); } say();//eric
因?yàn)閮?nèi)部的沒(méi)有定義name變量,這個(gè)結(jié)果不出意料的我們都知道,但這個(gè)過(guò)程我把它模擬成以下查找過(guò)程:
//從當(dāng)前函數(shù)的活動(dòng)對(duì)象開(kāi)始,一層一層向上查找,直到頂層全局作用域 //break這句相當(dāng)重要,當(dāng)前這一層找到了,不再向上一層找了。即在這一層環(huán)境中找到了變量name for (var i=0;i我覺(jué)得這段代碼,可以非常形象的表達(dá)了作用域鏈的查找過(guò)程,
即首先查找第一個(gè)變量對(duì)象,其實(shí)就是函數(shù)內(nèi)部的活動(dòng)對(duì)象,如果找到則不進(jìn)行下一個(gè)變量對(duì)象的查找,如果內(nèi)部函數(shù)沒(méi)有,才會(huì)沿著作用域鏈找下一個(gè)值,直到頂層的全局環(huán)境。這就是為什么我用數(shù)組去模擬作用域鏈的原因,因?yàn)?b>作用域鏈可以理解是個(gè)有序列表(其實(shí)作用域鏈的本質(zhì)就是指向變量對(duì)象的指針列表),查找過(guò)程是按順序查找的。
通過(guò)上面的形象化解釋?zhuān)遣皇欠浅:美斫庾饔糜蚝妥饔糜蜴溋四兀。。?/p> 垃圾回收
我們都知道在函數(shù)執(zhí)行完畢之后,內(nèi)部的變量和內(nèi)部定義的函數(shù)會(huì)隨之銷(xiāo)毀,也就是被垃圾回收機(jī)制所回收,如下:
function talk(){ var name = "eric"; function say(){ console.log(name); } say(); } talk();當(dāng)talk函數(shù)執(zhí)行后,內(nèi)部的變量name和聲明的函數(shù)say會(huì)從內(nèi)存中銷(xiāo)毀,但閉包的情況就不會(huì)。如:
function createTalk(){ var name = "eric"; var age = 99; return function (){ var innerName = name; console.log(innerName); } } var talk = createTalk(); talk();閉包中沒(méi)有釋放局部變量的原因閉包的本質(zhì)其實(shí)是有權(quán)訪問(wèn)另一個(gè)函數(shù)作用域中變量的函數(shù)
根據(jù)我們上面模擬的作用域鏈模型,上面的例子中當(dāng)talk執(zhí)行時(shí),整個(gè)作用域鏈可以形象化為:
ScopeChain = [ { arguments:[], this: window, innerName: undefined }, { arguments:[], this: window, name: eric, age: 99 }, { this: window, createTalk: function (){...}, talk: function (){...} //內(nèi)部return的匿名函數(shù) }, ]這樣當(dāng)createTalk執(zhí)行后,talk變量仍然保持了對(duì)函數(shù)內(nèi)部變量和內(nèi)部匿名函數(shù)的引用,因此即使createTalk執(zhí)行完畢,雖然其執(zhí)行環(huán)境被銷(xiāo)毀,但返回的匿名函數(shù)的作用域鏈被初始化為createTalk()函數(shù)的活動(dòng)對(duì)象和全局變量對(duì)象,內(nèi)部變量仍然沒(méi)有被垃圾回收機(jī)制所回收。雖然返回的匿名函數(shù),僅使用了外一層的name變量,而沒(méi)有使用age變量。但其內(nèi)部保存的仍然是整個(gè)外層變量對(duì)象,即
{ arguments:[], this: window, name: eric, age: 99 }而不僅僅是外層的name變量一個(gè)值,因?yàn)椴檎疫^(guò)程中,使用的是整個(gè)的變量對(duì)象來(lái)查找的。因?yàn)槭遣檎遥源嬖诒闅v整個(gè)對(duì)象的過(guò)程,而不是簡(jiǎn)單的賦值。
這就是為什么閉包會(huì)占用更多的內(nèi)存的原因,因?yàn)槠浔4媪苏麄€(gè)變量對(duì)象。雖然我們的例子可能就幾個(gè),但在實(shí)際應(yīng)用中可能存在非常多。
閉包的經(jīng)典實(shí)例
這也是我們要謹(jǐn)慎使用閉包的原因。接下來(lái)我們看一個(gè)經(jīng)典的閉包示例。
var result = []; for (var i=0;i<10;i++){ result[i] = function (){ return i; } }結(jié)果或許大家都知道了,result數(shù)組的任何一個(gè)執(zhí)行,都會(huì)返回10。下面我們用上面模擬的作用鏈,形象話的看下,
比如result[9]()函數(shù)執(zhí)行的初始化作用域鏈如下:ScopeChain = [ //第一層是內(nèi)部匿名函數(shù)的變量對(duì)象 { arguments:[], this: window }, //第二層是外部的,也就是全局變量對(duì)象 { this: window, result: [Array], i: 10 //此時(shí)全局環(huán)境的i已經(jīng)經(jīng)過(guò)for循環(huán)變成了10 }, ]自然任何一個(gè)result的值調(diào)用函數(shù),都會(huì)是返回10。
通過(guò)變形符合預(yù)期的閉包如下:var result = []; for (var i=0;i<10;i++){ result[i] = function (num){ return function (){ return num; } }(i); }上面這個(gè)經(jīng)典的閉包返回的就是我們想要的各自的i,為了更好理解,我還是使用形象的作用域鏈。
當(dāng)匿名函數(shù)執(zhí)行時(shí),看下它的初始作用域鏈:ScopeChain = [ //第一層為傳入?yún)?shù)i的自執(zhí)行函數(shù) { arguments:[], this: window, }, { arguments:[num], num: 9, this: window, } { this: window, result: [Array], i: 10 } ]我們可以理解為多了一層作用域鏈的變量對(duì)象,使其能保留對(duì)num副本的引用,而不是對(duì)i的引用。
好了,通過(guò)深入理解作用域鏈,我們能跟好的理解js的運(yùn)行機(jī)制和閉包的原理。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/90025.html
摘要:一概要作用域和作用域鏈?zhǔn)侵蟹浅V匾奶匦?,關(guān)系到理解整個(gè)體系,閉包是對(duì)作用域的延伸,其他語(yǔ)言也有閉包的特性。作用域鏈的作用他保證了變量對(duì)象的有序訪問(wèn)。 一、概要 作用域和作用域鏈?zhǔn)莏s中非常重要的特性,關(guān)系到理解整個(gè)js體系,閉包是對(duì)作用域的延伸,其他語(yǔ)言也有閉包的特性。 那什么是作用域?作用域指的是一個(gè)變量和函數(shù)的作用范圍。 1、js中函數(shù)內(nèi)聲明的所有變量在函數(shù)體內(nèi)始終是可見(jiàn)的; 2...
摘要:所以,全局執(zhí)行環(huán)境的變量對(duì)象始終都是作用域鏈中的最后一個(gè)對(duì)象。講到這里,可能你已經(jīng)對(duì)執(zhí)行環(huán)境執(zhí)行環(huán)境對(duì)象變量對(duì)象作用域作用域鏈的理解已經(jīng)他們之間的關(guān)系有了一個(gè)較清晰的認(rèn)識(shí)。 JavaScript中的執(zhí)行環(huán)境、作用域、作用域鏈、閉包一直是一個(gè)非常有意思的話題,很多博主和大神都分享過(guò)相關(guān)的文章。這些知識(shí)點(diǎn)不僅比較抽象,不易理解,更重要的是與這些知識(shí)點(diǎn)相關(guān)的問(wèn)題在面試中高頻出現(xiàn)。之前我也看過(guò)...
摘要:閉包面試題解由于作用域鏈機(jī)制的影響,閉包只能取得內(nèi)部函數(shù)的最后一個(gè)值,這引起的一個(gè)副作用就是如果內(nèi)部函數(shù)在一個(gè)循環(huán)中,那么變量的值始終為最后一個(gè)值。 (關(guān)注福利,關(guān)注本公眾號(hào)回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實(shí)戰(zhàn)、面試指導(dǎo)) 本周正式開(kāi)始前端進(jìn)階的第二期,本周的主題是作用域閉包,今天是第8天。 本計(jì)劃一共28期,每期重點(diǎn)攻克一個(gè)面試重難點(diǎn),如果你還不了...
摘要:圖片中的作用域鏈,是全局執(zhí)行環(huán)境中的作用域鏈。然后此活動(dòng)對(duì)象被推入作用域鏈的最前端。在最后調(diào)用的時(shí)候,創(chuàng)建先構(gòu)建作用域鏈,再創(chuàng)建執(zhí)行環(huán)境,再創(chuàng)建執(zhí)行環(huán)境的時(shí)候發(fā)現(xiàn)了一個(gè)變量標(biāo)識(shí)符。 從圖書(shū)館翻過(guò)各種JS的書(shū)之后,對(duì)作用域/執(zhí)行環(huán)境/閉包這些概念有了一個(gè)比較清晰的認(rèn)識(shí)。 栗子說(shuō)明一切 第一個(gè)栗子 來(lái)看一個(gè)來(lái)自ECMA-262的栗子: var x = 10; (function foo(...
摘要:并且作用域鏈也確定了在當(dāng)前上下文中查找標(biāo)識(shí)符后返回的值。為了具象化分析問(wèn)題,我們可以假設(shè)作用域鏈?zhǔn)且粋€(gè)數(shù)組,數(shù)組成員有一系列變量對(duì)象組成。注意,所有作用域鏈的最末端都為全局變量對(duì)象。所以作用域作用域鏈都是在當(dāng)前運(yùn)行環(huán)境內(nèi)代碼執(zhí)行前就確定了。 什么是作用域(Scope)? 作用域產(chǎn)生于程序源代碼中定義變量的區(qū)域,在程序編碼階段就確定了。javascript 中分為全局作用域(Global...
閱讀 1654·2019-08-30 15:55
閱讀 976·2019-08-30 15:44
閱讀 870·2019-08-30 10:48
閱讀 2039·2019-08-29 13:42
閱讀 3187·2019-08-29 11:16
閱讀 1253·2019-08-29 11:09
閱讀 2058·2019-08-26 11:46
閱讀 617·2019-08-26 11:44