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