摘要:所以,全局執行環境的變量對象始終都是作用域鏈中的最后一個對象。講到這里,可能你已經對執行環境執行環境對象變量對象作用域作用域鏈的理解已經他們之間的關系有了一個較清晰的認識。
JavaScript中的執行環境、作用域、作用域鏈、閉包一直是一個非常有意思的話題,很多博主和大神都分享過相關的文章。這些知識點不僅比較抽象,不易理解,更重要的是與這些知識點相關的問題在面試中高頻出現。之前我也看過不少文章,依舊是似懂非懂,模模糊糊。最近,仔細捋了捋相關問題的思路,對這些問題的理解清晰深入了不少,在這里和大家分享。
本文已同步至我的個人主頁。歡迎訪問查看更多內容!如有錯誤或遺漏,歡迎隨時指正探討!謝謝大家的關注與支持!
這篇文章,我會按照執行環境、作用域、作用域鏈、閉包的順序,結合著JS中函數的運行機制來梳理相關知識。因為這樣的順序剛好也是這些知識點相互關聯且遞進的順序,同時這些知識點都又與函數有著千絲萬縷的聯系。這樣講解,會更容易讓大家徹底理解,至少我就是這樣理解清晰的。
廢話不再多說,我們開始。
執行環境首先,我們還是要理解一下什么是執行環境,這也是理清后面問題的基礎。
執行環境是JavaScript中最為重要的一個概念。執行環境定義了變量或函數有權訪問的其他數據,決定了它們各自的行為。——《JavaScript高級程序設計》
抽象!不理解!沒關系,我來解釋:其實,執行環境就是JS中提出的一個概念,它是為了保證代碼合理運行采用的一種機制。
一種概念...機制...更抽象,那它到底是什么?實際上,執行環境在JS機制內部就是用一個對象來表示的,稱作執行環境對象,簡稱環境對象。
那么,這個執行環境對象到底又是何時、怎么產生的呢?有以下兩種情況:
在頁面中的腳本開始執行時,就會產生一個“全局執行環境”。它是最外圍(范圍最大,或者說層級最高)的一個執行環境,對應著一個全局環境對象。在Web瀏覽器中,這個對象就是Window對象。
當一個函數被調用的時候,也會創建一個屬于該函數的執行環境,稱作“局部執行環境”(或者稱作函數執行環境),它也對應著自己的環境對象。
因此,執行環境就分為全局執行環境和局部執行環境兩種,每個執行環境都有一個屬于自己的環境對象。
既然執行環境是使用一個對象表示的,那么對象就有屬性。我們來看看環境對象的三個有意思的屬性。變量對象、[[scope]]、this。
環境對象中的變量對象《JS高程》中明確說明,執行環境定義了變量或函數有權訪問的其他數據。那么這些數據到底被放(存儲)在哪里呢?
其實,每個執行環境都有一個與之關聯的變量對象,在環境中定義的所有變量和函數都保存在這個對象中。我們在代碼無法訪問這個對象,但解析器在處理數據時會在內部使用它。
通俗地說就是:一個執行環境中的所有變量和函數都保存在它對應的環境對象的變量對象(屬性)中。
認識[[scope]]前先理解作用域在講[[scope]]前,我們就需要先弄清楚什么是作用域了。因為作用域與[[scope]]之間存在著非常緊密的關系。
《JS高程》中沒有明確給出作用域的定義和描述。其實,作用域就是變量或者函數可以被訪問的代碼范圍,或者說作用域就是變量和函數所起作用的范圍。
這樣看來作用域也像是一個概念,它是用來描述一個區域(或者說范圍)的。在JS中,作用域分為全局作用域、局部作用域兩種。
我們來看看這兩種作用域的具體描述:
①在頁面中的腳本開始執行時,就會產生一個“全局作用域”。它是最外圍(范圍最大,或者說層級最高)的一個作用域。全局作用域的變量、函數
可以在代碼的任何地方訪問到。
②當一個函數被創建的時候,會創建一個“局部作用域”。局部作用域中的函數、變量只能在某些局部代碼中可以訪問到。
看一個例子:
var g = "Global"; function outer() { var out = "outer"; function inner() { var inn = "inner"; } }
上面這個例子,產生的作用域就如下圖所示:
請注意上面①、②這兩段話!!!是不是覺得很熟悉,似曾相識?!沒錯,這兩段話和介紹全局/局部執行環境(全局/局部環境對象)時候的描述幾乎一摸一樣!作用域是不是和環境對象有著千絲萬縷的聯系呢?與此同時,我們再仔細回憶一下:1、作用域就是變量或者函數可以被訪問的代碼范圍。2、一個執行環境中定義的所有變量和函數都保存在它對應的環境對象中。
結合上面所述,其實不難得出:盡管作用域的描述更像是一個概念,但如果一定要將它具象化,問它到底是什么東西,與執行環境有什么關系?其實,作用域所對應的(不是相等、等于)是環境對象中的變量對象。
明白了這些,我們就可以來看看環境對象中的[[scope]]屬性。
環境對象中的[[scope]]首先,要明確的是,環境對象中的[[scope]]屬性值是一個指針,它指向該執行環境的作用域鏈。
到底什么是作用域鏈呢?作用域鏈本質上就是一個有序的列表,而列表中的每一項都是一個指向不同環境對象中的變量對象的指針。
那么,這個作用域鏈到底是怎么形成的呢?它里面指向變量對象的指針的順序又是如何規定的呢?我們用下面這個簡單的例子說明。
var g = "Hello"; function inner() { var inn = "Inner"; var res = g + inn; return res; } inner();
當執行了inner();這一行代碼后,代碼執行流進入inner函數內部,此時,JS內部會先創建inner函數的局部執行環境,然后創建該環境的作用域鏈。這個作用域鏈的最前端,就是inner執行環境自己的環境對象中的變量對象,作用域鏈第二項,就是全局環境的環境對象中的變量對象。這條作用域鏈如下圖所示:
形成了這樣的作用域鏈之后,就可以有秩序地訪問一個變量了。以這個例子為例:當執行inner();進入函數體內后,執行g + inn;一行,需要訪問變量g、inn,此時JS內部機制就會沿著這條作用域鏈查找所需變量。在當前inner函數的作用域中找到了變量inn,值為"Inner",查找終止。但是卻沒有找到變量g,于是沿著作用域鏈向上查找,進入全局作用域,在全局變量對象中找到了變量g,值為"Hello",查找終止。計算得出res為"HelloInner",并在最后返回結果。
與上面所講機制完全相同,如果是多層執行環境嵌套,則作用域鏈是這么形成的:
當代碼執行進入一個執行環境時,JS內部會開始創建該環境的作用域鏈。作用域鏈的最前端,始終都是當前執行環境的執行環境對象中的變量對象。如果這個環境是局部執行環境(函數執行環境),則將其活動對象作為變量對象。作用域鏈中的下一個是來自外層環境對象的變量對象,而再下一個則是來自再外層環境對象的變量對象...... 這樣一直延續到全局環境對象的變量對象。所以,全局執行環境的變量對象始終都是作用域鏈中的最后一個對象。
講到這里,可能你已經對執行環境、執行環境對象、變量對象、作用域、作用域鏈的理解已經他們之間的關系有了一個較清晰的認識。也有可能,對這么多的抽象問題還是有些懵懵懂懂。沒關系,我們用下面這一張圖,將上面的所有內容串聯起來,來直觀感受和理解他們。
var g = "Global"; function outer() { var out = "outer"; function inner() { var inn = "inner"; } inner(); } outer();
對于這張圖,有一些需要注意的地方:
當函數調用時,才會創建函數的執行環境和它的環境對象,再創建函數的活動對象,再創建函數環境的作用域鏈。
上圖中間一列變量對象中,outer、inner的變量對象其實是該函數的活動對象。全局環境是沒有活動對象的,只有在函數環境中,才會使用函數的活動對象來作為它的變量對象。
函數的活動對象,是在函數創建時使用函數內置的arguments類數組和其他命名參數來初始化的。所以實際上,函數的變量對象中應該還包含一個指向arguments類數組的指針。
有了對作用域、作用域鏈的理解,最后,我們來說一說閉包。
閉包 什么是閉包閉包就是有權訪問另一個函數作用域中的變量的函數。——《JavaScript高級程序設計》
對于閉包,最簡單的大白話可以這么理解:
①外部函數聲明內部函數,內部函數引用外部函數的局部變量,這些變量不會被釋放!——這是我曾經看到的別人的說法
或者這么理解:
②當在一個函數中返回另一個函數的時候(是返回一個函數,不是返回函數的調用或者函數的執行結果),就會形成閉包,被返回的這個函數就叫做閉包函數。——這是我自己的理解
上面兩句話看似不同,其實本質是一樣的。來看一個最簡單的閉包的例子:
function sum() { var num1 = 100; // 這里返回的是函數(體),不是函數的調用 return function(num2) { return num1 + num2; } } // 此時result指向sum返回的那個匿名函數 // 注意!此時該匿名函數并沒有被執行 let result = sum(); result(200);
那么,上面幾行代碼,為什么就會形成閉包呢?我們來分析一下,代碼執行中JS內部到底做了什么?
首先,有一點必須明確,就是一般情況下,一個函數執行完內部的代碼,函數調用時所創建的執行環境、環境對象(包括變量對象、[[scope]]等)都會被銷毀,它們的生命周期就只有函數調用到函數執行結束這一段時間。
但是上面的例子,就會出現例外。
當執行sum()時,調用該函數,創建它的環境對象,其中作用域鏈中第一項是自己環境的變量對象,第二項是全局環境的變量對象。當創建匿名函數的時候,也會創建匿名函數的環境對象,其中作用域鏈第一項是自己環境的變量對象,第二項是sum環境的變量對象,第三項是全局變量對象。
這時,問題就來了。按說,當函數sum執行完return之后,他的執行環境、變量對象、作用域鏈都會被銷毀。可是這時候卻不能銷毀他的變量對象,因為返回的匿名函數(此時由result指向該函數)并沒有執行,這個匿名函數的作用域鏈中還引用著sum函數的變量對象。換句話說,即使,sum執行完了,其執行環境的作用域鏈會被銷毀,但是它的變量對象還會保存在內存中,我們在sum函數外部,還能訪問到它內部的變量num1、num2,這就是形成閉包的真正原因。但是,當result()執行完后,這些變量對象、作用域鏈就會被銷毀。
閉包存在的問題因為閉包形成后,會在函數執行完仍將他的變量對象保存在內存中,當引用時間過長或者引用對象很多的時候,會占用大量內存,嚴重影響性能。
來看下面的例子:(這個例子曾經是Tencent微眾銀行的筆試原題,出現在《JS高程》的7.2.3章節——P184)
function assignHandler() { var element = document.getElementById("someElement"); element.onclick = function(){ alert(element.id); }; }
assignHandler函數中定義的匿名函數是作為element元素的事件處理函數的,且內部使用了element元素(訪問元素的id`),因此assignHandler函數執行完,對于element的引用也會一直存在,element元素會一直保存在內存中。
將上面的例子改成下面這樣,就能解決這個問題。
function assignHandler(){ var element = document.getElementById("someElement"); // 這里獲取element的id,為其創建一個副本 // 這樣是為了在下面事件處理函數中解除對element元素的引用 var id = element.id; element.onclick = function(){ alert(id); }; // 將element置為null,斷開對element元素的引用 // 這樣方便垃圾回收機制回收element所占的內存 element = null; }
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/103131.html
摘要:之前一篇文章我們詳細說明了變量對象,而這里,我們將詳細說明作用域鏈。而的作用域鏈,則同時包含了這三個變量對象,所以的執行上下文可如下表示。下圖展示了閉包的作用域鏈。其中為當前的函數調用棧,為當前正在被執行的函數的作用域鏈,為當前的局部變量。 showImg(https://segmentfault.com/img/remote/1460000008329355);初學JavaScrip...
摘要:閉包一詞來源于以下兩者的結合要執行的代碼塊由于自由變量被包含在代碼塊中,這些自由變量以及它們引用的對象沒有被釋放和為自由變量提供綁定的計算環境作用域。在以及及以上等語言中都能找到對閉包不同程度的支持。 溫馨提示:作者的爬坑記錄,對你等大神完全沒有價值,別在我這浪費生命 閉包,好吃嗎 ? 第一次聽到這個詞,很不幸是在一次面試中,可想而知結果很細碎,從此閉包和跨域在我匱乏的前端知識中成為了...
摘要:并且作用域鏈也確定了在當前上下文中查找標識符后返回的值。為了具象化分析問題,我們可以假設作用域鏈是一個數組,數組成員有一系列變量對象組成。注意,所有作用域鏈的最末端都為全局變量對象。所以作用域作用域鏈都是在當前運行環境內代碼執行前就確定了。 什么是作用域(Scope)? 作用域產生于程序源代碼中定義變量的區域,在程序編碼階段就確定了。javascript 中分為全局作用域(Global...
摘要:閉包面試題解由于作用域鏈機制的影響,閉包只能取得內部函數的最后一個值,這引起的一個副作用就是如果內部函數在一個循環中,那么變量的值始終為最后一個值。 (關注福利,關注本公眾號回復[資料]領取優質前端視頻,包括Vue、React、Node源碼和實戰、面試指導) 本周正式開始前端進階的第二期,本周的主題是作用域閉包,今天是第8天。 本計劃一共28期,每期重點攻克一個面試重難點,如果你還不了...
閱讀 641·2021-09-22 10:02
閱讀 6374·2021-09-03 10:49
閱讀 568·2021-09-02 09:47
閱讀 2153·2019-08-30 15:53
閱讀 2931·2019-08-30 15:44
閱讀 902·2019-08-30 13:20
閱讀 1815·2019-08-29 16:32
閱讀 892·2019-08-29 12:46