摘要:深入系列第八篇,介紹理論上的閉包和實踐上的閉包,以及從作用域鏈的角度解析經典的閉包題。定義對閉包的定義為閉包是指那些能夠訪問自由變量的函數。
定義JavaScript深入系列第八篇,介紹理論上的閉包和實踐上的閉包,以及從作用域鏈的角度解析經典的閉包題。
MDN 對閉包的定義為:
閉包是指那些能夠訪問自由變量的函數。
那什么是自由變量呢?
自由變量是指在函數中使用的,但既不是函數參數也不是函數的局部變量的變量。
由此,我們可以看出閉包共有兩部分組成:
閉包 = 函數 + 函數能夠訪問的自由變量
舉個例子:
var a = 1; function foo() { console.log(a); } foo();
foo 函數可以訪問變量 a,但是 a 既不是 foo 函數的局部變量,也不是 foo 函數的參數,所以 a 就是自由變量。
那么,函數 foo + foo 函數訪問的自由變量 a 不就是構成了一個閉包嘛……
還真是這樣的!
所以在《JavaScript權威指南》中就講到:從技術的角度講,所有的JavaScript函數都是閉包。
咦,這怎么跟我們平時看到的講到的閉包不一樣呢!?
別著急,這是理論上的閉包,其實還有一個實踐角度上的閉包,讓我們看看湯姆大叔翻譯的關于閉包的文章中的定義:
ECMAScript中,閉包指的是:
從理論角度:所有的函數。因為它們都在創建的時候就將上層上下文的數據保存起來了。哪怕是簡單的全局變量也是如此,因為函數中訪問全局變量就相當于是在訪問自由變量,這個時候使用最外層的作用域。
從實踐角度:以下函數才算是閉包:
即使創建它的上下文已經銷毀,它仍然存在(比如,內部函數從父函數中返回)
在代碼中引用了自由變量
接下來就來講講實踐上的閉包。
分析讓我們先寫個例子,例子依然是來自《JavaScript權威指南》,稍微做點改動:
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } var foo = checkscope(); foo();
首先我們要分析一下這段代碼中執行上下文棧和執行上下文的變化情況。
另一個與這段代碼相似的例子,在《JavaScript深入之執行上下文》中有著非常詳細的分析。如果看不懂以下的執行過程,建議先閱讀這篇文章。
這里直接給出簡要的執行過程:
進入全局代碼,創建全局執行上下文,全局執行上下文壓入執行上下文棧
全局執行上下文初始化
執行 checkscope 函數,創建 checkscope 函數執行上下文,checkscope 執行上下文被壓入執行上下文棧
checkscope 執行上下文初始化,創建變量對象、作用域鏈、this等
checkscope 函數執行完畢,checkscope 執行上下文從執行上下文棧中彈出
執行 f 函數,創建 f 函數執行上下文,f 執行上下文被壓入執行上下文棧
f 執行上下文初始化,創建變量對象、作用域鏈、this等
f 函數執行完畢,f 函數上下文從執行上下文棧中彈出
了解到這個過程,我們應該思考一個問題,那就是:
當 f 函數執行的時候,checkscope 函數上下文已經被銷毀了啊(即從執行上下文棧中被彈出),怎么還會讀取到 checkscope 作用域下的 scope 值呢?
以上的代碼,要是轉換成 PHP,就會報錯,因為在 PHP 中,f 函數只能讀取到自己作用域和全局作用域里的值,所以讀不到 checkscope 下的 scope 值。(這段我問的PHP同事……)
然而 JavaScript 卻是可以的!
當我們了解了具體的執行過程后,我們知道 f 執行上下文維護了一個作用域鏈:
fContext = { Scope: [AO, checkscopeContext.AO, globalContext.VO], }
對的,就是因為這個作用域鏈,f 函數依然可以讀取到 checkscopeContext.AO 的值,說明當 f 函數引用了 checkscopeContext.AO 中的值的時候,即使 checkscopeContext 被銷毀了,但是 JavaScript 依然會讓 checkscopeContext.AO 活在內存中,f 函數依然可以通過 f 函數的作用域鏈找到它,正是因為 JavaScript 做到了這一點,從而實現了閉包這個概念。
所以,讓我們再看一遍實踐角度上閉包的定義:
即使創建它的上下文已經銷毀,它仍然存在(比如,內部函數從父函數中返回)
在代碼中引用了自由變量
在這里再補充一個《JavaScript權威指南》英文原版對閉包的定義:
This combination of a function object and a scope (a set of variable bindings) in which the function’s variables are resolved is called a closure in the computer science literature.
閉包在計算機科學中也只是一個普通的概念,大家不要去想得太復雜。
必刷題接下來,看這道刷題必刷,面試必考的閉包題:
var data = []; for (var i = 0; i < 3; i++) { data[i] = function () { console.log(i); }; } data[0](); data[1](); data[2]();
答案是都是 3,讓我們分析一下原因:
當執行到 data[0] 函數之前,此時全局上下文的 VO 為:
globalContext = { VO: { data: [...], i: 3 } }
當執行 data[0] 函數的時候,data[0] 函數的作用域鏈為:
data[0]Context = { Scope: [AO, globalContext.VO] }
data[0]Context 的 AO 并沒有 i 值,所以會從 globalContext.VO 中查找,i 為 3,所以打印的結果就是 3。
data[1] 和 data[2] 是一樣的道理。
所以讓我們改成閉包看看:
var data = []; for (var i = 0; i < 3; i++) { data[i] = (function (i) { return function(){ console.log(i); } })(i); } data[0](); data[1](); data[2]();
當執行到 data[0] 函數之前,此時全局上下文的 VO 為:
globalContext = { VO: { data: [...], i: 3 } }
跟沒改之前一模一樣。
當執行 data[0] 函數的時候,data[0] 函數的作用域鏈發生了改變:
data[0]Context = { Scope: [AO, 匿名函數Context.AO globalContext.VO] }
匿名函數執行上下文的 AO 為:
匿名函數Context = { AO: { arguments: { 0: 0, length: 1 }, i: 0 } }
data[0]Context 的 AO 并沒有 i 值,所以會沿著作用域鏈從匿名函數 Context.AO 中查找,這時候就會找 i 為 0,找到了就不會往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值為3),所以打印的結果就是 0。
data[1] 和 data[2] 是一樣的道理。
下一篇文章JavaScript深入之參數按值傳遞
相關鏈接如果想了解執行上下文的具體變化,不妨循序漸進,閱讀這六篇:
《JavaScript深入之詞法作用域和動態作用域》
《JavaScript深入之執行上下文棧》
《JavaScript深入之變量對象》
《JavaScript深入之作用域鏈》
《JavaScript深入之從ECMAScript規范解讀this》
《JavaScript深入之執行上下文》
深入系列JavaScript深入系列目錄地址:https://github.com/mqyqingfeng/Blog。
JavaScript深入系列預計寫十五篇左右,旨在幫大家捋順JavaScript底層知識,重點講解如原型、作用域、執行上下文、變量對象、this、閉包、按值傳遞、call、apply、bind、new、繼承等難點概念。
如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎star,對作者也是一種鼓勵。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/82730.html
摘要:使用上一篇文章的例子來說明下自由變量進階期深入淺出圖解作用域鏈和閉包訪問外部的今天是今天是其中既不是參數,也不是局部變量,所以是自由變量。 (關注福利,關注本公眾號回復[資料]領取優質前端視頻,包括Vue、React、Node源碼和實戰、面試指導) 本周正式開始前端進階的第二期,本周的主題是作用域閉包,今天是第7天。 本計劃一共28期,每期重點攻克一個面試重難點,如果你還不了解本進階計...
摘要:閉包面試題解由于作用域鏈機制的影響,閉包只能取得內部函數的最后一個值,這引起的一個副作用就是如果內部函數在一個循環中,那么變量的值始終為最后一個值。 (關注福利,關注本公眾號回復[資料]領取優質前端視頻,包括Vue、React、Node源碼和實戰、面試指導) 本周正式開始前端進階的第二期,本周的主題是作用域閉包,今天是第8天。 本計劃一共28期,每期重點攻克一個面試重難點,如果你還不了...
摘要:閉包一認識閉包閉包是一種特殊的對象。它由兩部分構成函數,以及創建該函數的環境包含自由變量。環境由閉包創建時在作用域中的任何局部變量組成。創建閉包最常見方式,就是在一個函數內部創建另一個函數。 閉包 showImg(https://segmentfault.com/img/bVbe3nk?w=1335&h=653); 一、認識閉包 閉包是一種特殊的對象。它由兩部分構成:函數,以及創建該函...
摘要:本期推薦文章從作用域鏈談閉包,由于微信不能訪問外鏈,點擊閱讀原文就可以啦。推薦理由這是一篇譯文,深入淺出圖解作用域鏈,一步步深入介紹閉包。作用域鏈的頂端是全局對象,在全局環境中定義的變量就會綁定到全局對象中。 (關注福利,關注本公眾號回復[資料]領取優質前端視頻,包括Vue、React、Node源碼和實戰、面試指導) 本周開始前端進階的第二期,本周的主題是作用域閉包,今天是第6天。 本...
摘要:理解閉包概念閉包是指有權訪問另一個函數作用域中的變量的函數。閉包在執行后,仍然可以訪問內部的,因為將的內的活動對象添加到了的作用域鏈。閉包的應用監聽事件事件錯誤的使用循環使用閉包封裝函數,便于使用私有變量。 理解閉包 概念 閉包是指 有權訪問另一個函數作用域中的變量的 函數。 函數式閉包(在內部保存數據和對外部無副作用) 創建方法 在一個函數內部創建另一個函數(閉包) 原理 普通函數:...
閱讀 3093·2021-11-22 09:34
閱讀 593·2021-11-22 09:34
閱讀 2436·2021-10-08 10:18
閱讀 3372·2021-09-22 15:57
閱讀 2585·2021-09-22 15:25
閱讀 2398·2019-08-30 15:54
閱讀 2092·2019-08-30 15:44
閱讀 1799·2019-08-29 11:18