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