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