淺析JavaScript作用域鏈、執(zhí)行上下文與閉包
閉包和作用域鏈?zhǔn)荍avaScript中比較重要的概念,這兩天翻閱了一些資料,把相關(guān)知識(shí)點(diǎn)給大家總結(jié)了以下。
JavaScript 采用詞法作用域(lexical scoping),函數(shù)執(zhí)行依賴(lài)的變量作用域是由函數(shù)定義的時(shí)候決定,而不是函數(shù)執(zhí)行的時(shí)候決定。以下面的代碼片段舉例說(shuō)明,通常來(lái)說(shuō)(基于棧的實(shí)現(xiàn),如 C 語(yǔ)言) foo 被調(diào)用之后函數(shù)內(nèi)的本地變量 scope 會(huì)被釋放,但是從詞法上看 foo 的內(nèi)嵌匿名函數(shù)中 scope 應(yīng)該指的是 foo 的本地變量 scope ,并且實(shí)際上代碼的運(yùn)行結(jié)果跟詞法上的表達(dá)式一致的,f 被調(diào)用之后返回的是local scope。函數(shù)對(duì)象 f 在其主體函數(shù) foo 調(diào)用結(jié)束之后,依然保持著 foo 函數(shù)體作用域變量的引用,這就是所謂的閉包 。
var scope = 'global scope'; function foo() { var scope = 'local scope'; return function () { return scope; } } var f = foo(); f(); // 返回 "local scope"
那么閉包到底是如何工作的呢?了解閉包首先需要了解變量作用域和作用域鏈,另外一個(gè)重要的概念是執(zhí)行上下文環(huán)境。
變量作用域
JavaScript 中全局變量擁有全局的作用域,函數(shù)體內(nèi)申明的變量的作用域是整個(gè)函數(shù)體內(nèi),是局部的,當(dāng)然也包括函數(shù)體內(nèi)定義的嵌套函數(shù)。函數(shù)體內(nèi)局部變量的優(yōu)先級(jí)高于全局變量,如果局部變量與全局變量重名,全局變量會(huì)被局部變量掩蓋;同樣嵌套函數(shù)內(nèi)定義的局部變量的優(yōu)先級(jí)高于嵌套函數(shù)所在函數(shù)的局部變量。這簡(jiǎn)直是顯而易見(jiàn)的,幾乎所有人都了解。
接下來(lái)談?wù)効赡艽蠹冶容^陌生的。
函數(shù)聲明提升
用一句話(huà)來(lái)說(shuō)明函數(shù)申明提升,指的是函數(shù)體內(nèi)部申明的變量再整個(gè)函數(shù)內(nèi)有效。也就是說(shuō),就是在函數(shù)體最底部申明的變量,也會(huì)被提升到最頂部。舉個(gè)例子:
var scope = 'global scope'; function foo() { console.log(scope); // 這里不會(huì)打印出 "global scope",而是 "undefined" var scope = 'local scope'; console.log(scope); // 很顯然,打印出 "local scope" } foo();
第一個(gè)console.log(scope)會(huì)打印出undefined而不是global scope,是因?yàn)榫植孔兞康纳昝鞅惶嵘?,只是還未賦值。
作為屬性的變量
在 JavaScript 中,有三種定義全局變量的方式,如下示例代碼中的 globalVal1 、globalVal2 和 globalValue3 。一個(gè)有趣的現(xiàn)象是,實(shí)際上全局變量?jī)H僅只是全局對(duì)象 window/global (在瀏覽器中是 window,在 node.js 中是 global)的屬性而已。為了更加符合通常意義的變量定義, JavaScript 把用 var 定義的全局變量,設(shè)計(jì)成了不可刪除的全局對(duì)象屬性。 通過(guò)Object.getOwnPropertyDescriptor(this, 'globalVal1')可以得到,其 configurable 屬性為 false 。
var globalVal1 = 1; // 不可刪除的全局變量 globalVal2 = 2; // 可刪除的全局變量 this.globalValue3 = 3; // 同 globalValue2 delete globalVal1; // => false 變量沒(méi)有被刪除 delete globalVal2; // => true 變量被刪除 delete this.globalValue3; //=> true 變量被刪除
那么問(wèn)題來(lái)了,函數(shù)體內(nèi)定義的局部變量是不是也作為某個(gè)對(duì)象的屬性呢?答案是肯定的。這個(gè)對(duì)象是跟函數(shù)調(diào)用相關(guān)的,在 ECMAScript 3中稱(chēng)為“call object”、ECMAScript 5中稱(chēng)為“declaravite environment record”的對(duì)象。這個(gè)特殊的對(duì)象對(duì)我們來(lái)說(shuō)是一種不可見(jiàn)的內(nèi)部實(shí)現(xiàn)。
作用域鏈
從上一節(jié)我們知道,函數(shù)局部變量可與看做是某個(gè)不可見(jiàn)的對(duì)象的屬性。那么 JavaScript 的詞法作用域的實(shí)現(xiàn)可以這樣描述:每一段 JavaScript 代碼(全局或函數(shù))都有一個(gè)跟它關(guān)聯(lián)的作用域鏈,它可以是數(shù)組或鏈表結(jié)構(gòu);作用域鏈中的每一個(gè)元素定義了一組作用域內(nèi)的變量;當(dāng)我們要查找變量 x 的值,那么從作用域鏈的第一個(gè)元素中找這個(gè)變量,如果沒(méi)有找到者找鏈表中的下一個(gè)元素中查找,直到找到或抵達(dá)鏈尾。了解作用域鏈的概念對(duì)理解閉包至關(guān)重要。
執(zhí)行上下文
每段 JavaScript 代碼的執(zhí)行都與執(zhí)行上下文綁定,運(yùn)行的代碼通過(guò)執(zhí)行上下文獲可用的變量、函數(shù)、數(shù)據(jù)等信息。全局的執(zhí)行上下文是唯一的,與全局代碼綁定,每執(zhí)行一個(gè)函數(shù)都會(huì)創(chuàng)建一個(gè)執(zhí)行上下文與其綁定。JavaScript 通過(guò)棧的數(shù)據(jù)結(jié)構(gòu)維護(hù)執(zhí)行上下文,全局執(zhí)行上下文位于棧底,當(dāng)執(zhí)行一個(gè)函數(shù)的時(shí)候,新創(chuàng)建的函數(shù)執(zhí)行上下文將會(huì)壓入棧中,執(zhí)行上下文指針指向棧頂,運(yùn)行的代碼即可獲得當(dāng)前執(zhí)行的函數(shù)綁定的執(zhí)行上下文。如果函數(shù)體執(zhí)行嵌套的函數(shù),也會(huì)創(chuàng)建執(zhí)行上下文并壓入棧,指針指向棧頂,當(dāng)嵌套函數(shù)運(yùn)行結(jié)束后,與它綁定的執(zhí)行上下文被推出棧,指針重新指向函數(shù)綁定的執(zhí)行上下文。同樣,函數(shù)執(zhí)行結(jié)束,指針會(huì)指向全局執(zhí)行上下文。
執(zhí)行上下文可以描述成式一個(gè)包含變量對(duì)象(對(duì)應(yīng)全局)/活動(dòng)對(duì)象(對(duì)應(yīng)函數(shù))、作用域鏈和 this 的數(shù)據(jù)結(jié)構(gòu)。當(dāng)一個(gè)函數(shù)執(zhí)行時(shí),活動(dòng)對(duì)象被創(chuàng)建并綁定到執(zhí)行上下文?;顒?dòng)對(duì)象包括函數(shù)體內(nèi)申明的變量、函數(shù)、arguments 等。作用域鏈在上一節(jié)以及提到,是按詞法作用域構(gòu)建的。需要注意的是 this 不屬于活動(dòng)對(duì)象,在函數(shù)執(zhí)行的那一刻就以及確定。
執(zhí)行上下文的創(chuàng)建是有特定的次序和階段的,不同階段有不同的狀態(tài),具體的細(xì)節(jié)可以看一下參考資料,在結(jié)尾部分會(huì)列出。
閉包
了解了作用域鏈和執(zhí)行上下文,回過(guò)頭看篇首的那段代碼,基本上就可以解釋閉包式如何工作了。函數(shù)調(diào)用的時(shí)候創(chuàng)建的執(zhí)行上下文以及詞法作用域鏈保持函數(shù)調(diào)用所需要的信息, f 函數(shù)調(diào)用之后才可以返回local scope。
需要注意的是,函數(shù)內(nèi)定義的多個(gè)函數(shù)使用的是同一個(gè)作用域鏈,在使用 for 循環(huán)賦值匿名函數(shù)對(duì)象的場(chǎng)景比較容易引起錯(cuò)誤,舉例如下:
var arr = []; for (var i = 0; i < 10; i++) { arr[i] = { func: function() { return i; } }; } arr[0].func(); // 返回 10,而不是 0
arr[0].func()返回的是 10 而不是 0,跟感官上的語(yǔ)義有偏差。在 ECMAScript 6 引入 let 之前, 變量作用域范圍是在整個(gè)函數(shù)體內(nèi)而不是在代碼區(qū)塊之內(nèi),所以上面的例子中所有定義的 func 函數(shù)引用了同一個(gè)作用域鏈在 for 循環(huán)之后, i 的值已經(jīng)變?yōu)?10 。
正確的做法是這樣:
var arr = []; for (var i = 0; i < 10; i++) { arr[i] = { func: getFunc(i) }; } function getFunc(i) { return function() { return i; } } arr[0].func(); // 返回 0
以上內(nèi)容給大家介紹了JavaScript作用域鏈、執(zhí)行上下文與閉包的相關(guān)知識(shí),希望對(duì)大家有所幫助。
相關(guān)文章
disable-devtool禁用web開(kāi)發(fā)者工具保護(hù)網(wǎng)頁(yè)源碼
這篇文章主要為大家介紹了disable-devtool禁用web開(kāi)發(fā)者工具保護(hù)網(wǎng)頁(yè)源碼的使用,防止源碼泄露保護(hù)網(wǎng)站源碼的最佳解決方案,一行代碼就可以搞定,有需要的可以學(xué)習(xí)參考下2023-11-11微信小程序?qū)崿F(xiàn)表格前后臺(tái)分頁(yè)
這篇文章主要為大家詳細(xì)介紹了微信小程序?qū)崿F(xiàn)表格前后臺(tái)分頁(yè),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08使用javascript:將其它類(lèi)型值轉(zhuǎn)換成布爾類(lèi)型值的解決方法詳解
本篇文章是對(duì)使用javascript:將其它類(lèi)型值轉(zhuǎn)換成布爾類(lèi)型值的解決方法進(jìn)行了詳細(xì)的分析介紹。需要的朋友參考下2013-05-05兩種方法實(shí)現(xiàn)文本框輸入內(nèi)容提示消失
第一種方法:基于HTML5 input標(biāo)簽的新特性 - placeholder 。另外,x-webkit-speech 屬性可以實(shí)現(xiàn)語(yǔ)音輸入功能;第二種方法: 用span模擬,定位span,借助JS鍵盤(pán)事件判斷輸入,確定span里的內(nèi)容顯示隱藏2013-03-03JS 設(shè)置Cookie 有效期 檢測(cè)cookie
這篇文章主要介紹了JS 設(shè)置Cookie 有效期 檢測(cè)cookie的相關(guān)資料,需要的朋友可以參考下2017-06-061分鐘快速了解js實(shí)現(xiàn)下載文件功能的4種方式
在前端開(kāi)發(fā)中,我們經(jīng)常需要實(shí)現(xiàn)文件下載功能,例如下載用戶(hù)上傳的圖片、用戶(hù)生成的文件等,這篇文章主要給大家介紹了關(guān)于如何通過(guò)1分鐘快速了解js實(shí)現(xiàn)下載文件功能的4種方式,需要的朋友可以參考下2024-03-03