摘要:而閉包的神奇之處正是可以阻止事情的發(fā)生。拜所聲明的位置所賜,它擁有涵蓋內(nèi)部作用域的閉包,使得該作用域能夠一直存活,以供在之后任何時(shí)間進(jìn)行引用。依然持有對(duì)該作用域的引用,而這個(gè)引用就叫閉包。
引子
先看一個(gè)問(wèn)題,下面兩個(gè)代碼片段會(huì)輸出什么?
// Snippet 1 a = 2; var a; console.log(a); // Snippet 2 console.log(a); var a = 2;
如果了解過(guò) JavaScript 變量提升相關(guān)語(yǔ)法的話,答案是顯而易見(jiàn)的。本文作為《你不知道的 JavaScript》第一部分的閱讀筆記,順便來(lái)總結(jié)一下對(duì)作用域與閉包的理解。
一、先有蛋還是先有雞上面問(wèn)題的答案是:
-> 2
-> undefined
我們從編譯器的角度思考:
引擎會(huì)在解釋 JavaScript 代碼之前首先對(duì)其進(jìn)行編譯(沒(méi)錯(cuò),JavaScript 也是要進(jìn)行編譯的?。?,而編譯階段中的一部分工作就是找到所有聲明,并用合適的作用域?qū)⑺麄冴P(guān)聯(lián)起來(lái),即 包括變量和函數(shù)在內(nèi)的所有聲明都會(huì)在任何代碼被執(zhí)行前首先被處理。
當(dāng)你看到 var a = 2;時(shí)可能會(huì)認(rèn)為這是一個(gè)聲明,但 JavaScript 實(shí)際上會(huì)將其看成兩個(gè)聲明:var a 和 a = 2,第一個(gè)定義聲明是在編譯階段進(jìn)行的,第二個(gè)賦值聲明會(huì)被留在原地等待執(zhí)行階段處理。
打個(gè)比方,這個(gè)過(guò)程就好像變量和函數(shù)聲明從它們的代碼中出現(xiàn)的位置被“移動(dòng)”到了最上面,這個(gè)過(guò)程就叫做 提升。
所以,編譯之后上面兩個(gè)代碼片段是這樣的:
// Snippet 1 編譯后 var a; a = 2; console.log(a); // -> 2 // Snippet 2 編譯后 var a; console.log(a); // -> undefined a = 2;
所以結(jié)論就是:先有蛋(聲明),后有雞(賦值)。
二、編譯實(shí)際上,JavaScript 也是一門(mén)編譯語(yǔ)言。與傳統(tǒng)編譯語(yǔ)言的過(guò)程一樣,程序中的一段源代碼在執(zhí)行之前會(huì)經(jīng)過(guò)是三個(gè)步驟,統(tǒng)稱為“編譯”:
分詞/詞法分析(Tokenizing/Lexing)
解析/語(yǔ)法分析(Parsing)
代碼生成
簡(jiǎn)單來(lái)說(shuō),任何 JavaScript 代碼片段在執(zhí)行前都要進(jìn)行編譯(通常就在執(zhí)行前)。
三、作用域為了理解作用域,可以想象出有以下三種角色:
引擎:從頭到尾負(fù)責(zé)整個(gè) JavaScript 程序的編譯及執(zhí)行過(guò)程。
編譯器:引擎的好朋友之一,負(fù)責(zé)語(yǔ)法分析及代碼生成等臟活累活。
作用域:引擎的另一位好朋友,負(fù)責(zé)收集并維護(hù)所有聲明的標(biāo)識(shí)符(變量)組成的一系列查詢,并實(shí)施一套非常嚴(yán)格的規(guī)則,確定當(dāng)前執(zhí)行的代碼對(duì)這些標(biāo)識(shí)符的訪問(wèn)權(quán)限。
以 var a = 2; 為例,過(guò)程如下:
首先遇到 var a,編譯器會(huì)詢問(wèn)作用域是否已經(jīng)有一個(gè)名為 a 的變量存在于同一個(gè)作用域的集合中。如果是,編譯器會(huì)忽略該聲明,繼續(xù)進(jìn)行編譯;否則就會(huì)要求作用域在當(dāng)前作用域的集合中聲明一個(gè)新的變量,并命名為 a.
然后,編譯器會(huì)為引擎生成運(yùn)行時(shí)所需的代碼,這些代碼被用來(lái)處理 a=2 這個(gè)賦值操作。引擎運(yùn)行時(shí)會(huì)首選詢問(wèn)作用域,在當(dāng)前的作用域集合中是否存在一個(gè)叫做 a 的變量。如果是,引擎就會(huì)使用這個(gè)變量;如果否,引擎就會(huì)繼續(xù)查找該變量(一層一層向上查找)。
最后,如果引擎最終找到了a變量,就會(huì)將 2 賦值給它,否則引擎就會(huì)舉手示意并拋出一個(gè)異常(ReferenceError)!
當(dāng)一個(gè)塊或函數(shù)嵌套在另一個(gè)塊或函數(shù)中時(shí),就發(fā)生了作用域的嵌套。遍歷嵌套作用域鏈的規(guī)則很簡(jiǎn)單:引擎從當(dāng)前的執(zhí)行作用域開(kāi)始查找變量,如果找不到,就向上一級(jí)查找。當(dāng)?shù)诌_(dá)最外層的全局作用域時(shí),無(wú)論找到還是沒(méi)找到,查找過(guò)程都會(huì)停止。
四、函數(shù)聲明式 & 函數(shù)表達(dá)式JavaScript 中創(chuàng)建函數(shù)有兩種方式:
// 函數(shù)聲明式 function funcDeclaration() { return "A function declaration"; } // 函數(shù)表達(dá)式 var funcExpression = function () { return "A function expression"; }
聲明式與表達(dá)式的差異:
類似于 var 聲明,函數(shù)聲明可以 提升 到其它代碼之前,但函數(shù)表達(dá)式不能,不過(guò)允許保留在本地變量范圍內(nèi);
函數(shù)表達(dá)式可以匿名,而函數(shù)聲明不可以。
怎么判斷是函數(shù)聲明式還是函數(shù)表達(dá)式?
一個(gè)最簡(jiǎn)單的方法是看 function 關(guān)鍵字出現(xiàn)在聲明的位置,如果是在第一個(gè)詞,那么就是函數(shù)聲明式,否則就是函數(shù)表達(dá)式。
函數(shù)表達(dá)式比函數(shù)聲明式更加有用的地方:
是一個(gè)閉包
可以作為其他函數(shù)的參數(shù)
可以作為立即調(diào)用函數(shù)表達(dá)式(IIFE)
可以作為回調(diào)函數(shù)
五、匿名函數(shù) & 立即調(diào)用函數(shù)“在任意代碼片段外部添加包裝函數(shù),可以將內(nèi)部的變量和函數(shù)定義“隱藏起來(lái)”,外部作用域就無(wú)法訪問(wèn)包裝函數(shù)內(nèi)部的任何內(nèi)容。那么,能否更徹底一些?如果必須聲明一個(gè)有具體名字的函數(shù),這個(gè)名字本身就會(huì)“污染”所在作用域;其次,必須顯式通過(guò)函數(shù)名調(diào)用這個(gè)函數(shù)才能運(yùn)行其中的代碼。如果函數(shù)不需要函數(shù)名(或者至少函數(shù)名可以不污染所在作用域),并且能夠自動(dòng)運(yùn)行,這就完美了!”——論匿名函數(shù)和理解調(diào)用函數(shù)的誕生。
匿名函數(shù)表達(dá)式最熟悉的場(chǎng)景就是回調(diào)函數(shù):
setTimeout(function(){ console.log("I waited 1 second!"); }, 1000);
匿名函數(shù)表達(dá)式書(shū)寫(xiě)起來(lái)簡(jiǎn)單快捷,很多庫(kù)和工具也傾向鼓勵(lì)使用這種風(fēng)格的代碼。但是,它也有幾個(gè)缺點(diǎn)需要考慮:
匿名函數(shù)在棧追蹤中不會(huì)顯示出有意義的函數(shù)名,使得調(diào)試很困難。
如果沒(méi)有函數(shù)名,當(dāng)函數(shù)需要引用自身時(shí)只能使用已經(jīng)過(guò)期的 arguments.callee 引用,比如在遞歸中。另一個(gè)函數(shù)需要引用自身的例子,是在事件觸發(fā)后事件監(jiān)聽(tīng)器需要解綁自身。
匿名函數(shù)省略了對(duì)于代碼可讀性、可理解性很重要的函數(shù)名。一個(gè)描述性的名稱可以讓代碼不言自明。
所以,始終給函數(shù)表達(dá)式命名是一個(gè)最佳實(shí)踐:
setTimeout(function timeoutHandler(){ console.log("I waited 1 second!"); });
由于函數(shù)被包含在一對(duì)()括號(hào)內(nèi)部,因此成為了一個(gè)表達(dá)式,通過(guò)在末尾加上另外一個(gè)()括號(hào)就可以立即執(zhí)行這個(gè)函數(shù),比如:
(function foo(){ // ... })()
第一個(gè)()將函數(shù)變成了表達(dá)式,第二個(gè)()執(zhí)行了這個(gè)函數(shù)。
它有個(gè)術(shù)語(yǔ):IIFE,表示:立即執(zhí)行函數(shù)表達(dá)式(Immediately Invoked Function Expression)。
它有另外一個(gè)改進(jìn)形式:
(function foo(){ // ... }())
不同點(diǎn)就是把最后的括號(hào)挪進(jìn)去了,實(shí)際上 這兩種形式在功能上是一致的,選擇哪個(gè)全憑個(gè)人喜好。
至于 IIFE 的另一個(gè)非常普遍的進(jìn)階用法是 把它們當(dāng)做函數(shù)調(diào)用并傳遞參數(shù)進(jìn)去:
var a = 2; (function foo(global){ var a = 3; console.log(a); // -> 3 console.log(global.a); // -> 2 })(window); // 傳入window對(duì)象的引用 console.log(a); // -> 2六、再談提升
現(xiàn)在我們?cè)賮?lái)談一談提升。
// Snippet 3 foo(); // -> TypeError bar(); // -> ReferenceError var foo = function bar(){ console.log(1); };
為什么會(huì)輸出上面這兩個(gè)異常?我們可以從編譯器的角度把代碼看出這樣子:
var foo; // 聲明提升 foo(); // 聲明但未定義為 undefined,然后這里進(jìn)行了函數(shù)調(diào)用,所以返回 TypeError bar(); // 無(wú)聲明拋出引用異常,所以返回 ReferenceError foo = function bar(){ console.log(1); };
然后再變化一下,同名的函數(shù)聲明和變量聲明在提升階段會(huì)怎么處理:
foo(); // 到底會(huì)輸出什么? var foo; function foo(){ console.log(1); } foo = function(){ console.log(2); }
上面代碼會(huì)被引擎理解為如下形式:
function foo(){ console.log(1); } foo(); // -> 1 foo = function(){ console.log(2); }
解釋:var foo 盡管出現(xiàn)在 function foo() 的聲明之前,但它是重復(fù)的聲明(因此被忽略了),因?yàn)楹瘮?shù)聲明會(huì)被提升到普通變量之前。即:函數(shù)聲明和變量聲明都會(huì)被提升,但函數(shù)會(huì)首先被提升,然后才是變量(這也從側(cè)面說(shuō)明了在 JavaScript 中“函數(shù)是一等公民”)。
再來(lái):
foo(); // -> 3 function foo(){ console.log(1); } var foo = function(){ console.log(2); } function foo(){ console.log(3); }
解釋:盡管重復(fù)的 var 聲明會(huì)被忽略掉,但出現(xiàn)后面的函數(shù)聲明還是可以覆蓋前面的。
七、閉包閉包是基于詞法作用域書(shū)寫(xiě)代碼時(shí)所產(chǎn)生的自然結(jié)果,你甚至不需要為了利用它們而有意識(shí)地創(chuàng)建閉包。閉包的創(chuàng)建和使用在你的代碼中隨處可見(jiàn)。你缺少的是根據(jù)你自己的意愿來(lái)識(shí)別、擁抱和影響閉包的思維環(huán)境。
當(dāng)函數(shù)可以記住并訪問(wèn)所在的詞法作用域時(shí),就產(chǎn)生了 閉包,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行。
function foo(){ var a = 2; function bar(){ console.log(a); } return bar; } var baz = foo(); baz(); // -> 2,閉包的效果!
以下是解釋說(shuō)明:
函數(shù) bar() 的詞法作用域能夠訪問(wèn) foo() 的內(nèi)部作用域,然后我們將 bar() 函數(shù)本身當(dāng)做一個(gè)值類型緊傳遞。在這個(gè)例子中,我們將 bar() 所引用的函數(shù)對(duì)象本身當(dāng)做返回值。
在 foo()執(zhí)行后,其返回值(也就是內(nèi)部的 bar() 函數(shù))賦值給變量 baz 并調(diào)用 baz(),實(shí)際上只是通過(guò)不同的標(biāo)識(shí)符引用調(diào)用了內(nèi)部的函數(shù) bar()。
bar() 顯示是可以被正常執(zhí)行,但是在這個(gè)例子中,它在自己定義的詞法作用域以外的地方執(zhí)行。
在 foo() 執(zhí)行后,通常會(huì)期待 foo() 的整個(gè)內(nèi)部作用域都被銷毀,因?yàn)槲覀冎酪嬗欣厥掌饔脕?lái)釋放不再使用的內(nèi)存空間。由于看上去 foo() 的內(nèi)容不會(huì)再被使用,所以很自然地會(huì)考慮對(duì)其進(jìn)行回收。
而閉包的神奇之處正是可以阻止事情的發(fā)生。事實(shí)上,內(nèi)部作用域依然存在,因此沒(méi)有被回收。誰(shuí)在使用這個(gè)內(nèi)部作用域?原來(lái)是 bar() 本身在使用。
拜 bar() 所聲明的位置所賜,它擁有涵蓋 foo() 內(nèi)部作用域的閉包,使得該作用域能夠一直存活,以供 bar() 在之后任何時(shí)間進(jìn)行引用。
bar() 依然持有對(duì)該作用域的引用,而 這個(gè)引用就叫閉包。
本質(zhì)上,無(wú)論何時(shí)何地,如果將函數(shù)(訪問(wèn)它們各自的詞法作用域)當(dāng)做第一級(jí)的值類型并到處傳遞,你就會(huì)看到閉包在這些函數(shù)中的應(yīng)用。在定時(shí)器、事件監(jiān)聽(tīng)器、Ajax 請(qǐng)求、跨窗口通信、Web Workers 或者任何其他的異步(或者同步)任務(wù)中,只要使用了回調(diào)函數(shù),實(shí)際上就是在使用閉包。
再補(bǔ)充一個(gè)示例:
function foo() { function bar() { console.log("1"); } function baz() { console.log("2"); } var yyy = { bar: bar, baz: baz } return yyy; } var kkk = foo(); // kkk通過(guò)foo獲得了yyy的引用,也就可以調(diào)用bar和baz kkk.bar(); // -> 1 kkk.baz(); // -> 2九、動(dòng)態(tài)作用域
事實(shí)上,JavaScript 并不具有動(dòng)態(tài)作用域,它只有 詞法作用域(雖然 this 機(jī)制某種程度上很像動(dòng)態(tài)作用域)。詞法作用域和動(dòng)態(tài)作用域的主要區(qū)別為:
詞法作用域是在寫(xiě)代碼或者定義時(shí)確定的,而動(dòng)態(tài)作用域是在運(yùn)行時(shí)確定的;
詞法作用域關(guān)注函數(shù)在何處聲明,而動(dòng)態(tài)作用域關(guān)注函數(shù)從何處調(diào)用。
像下面的代碼片段,如果是動(dòng)態(tài)作用域輸出的就是3而不是2了:
function foo(){ console.log(a); // -> 2 } function bar(){ var a = 3; foo(); } var a = 2; bar();十、參考
你不知道的 JavaScript(上卷)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/88139.html
摘要:一先有雞還有先有蛋直覺(jué)上會(huì)認(rèn)為代碼在執(zhí)行時(shí)是由上到下一行一行執(zhí)行的。不幸的是兩種猜測(cè)都是不對(duì)的。換句話說(shuō),我們的問(wèn)題先有雞還是先有蛋的結(jié)論是先有蛋聲明后有雞賦值。 一、先有雞還有先有蛋? 直覺(jué)上會(huì)認(rèn)為javascript代碼在執(zhí)行時(shí)是由上到下一行一行執(zhí)行的。但實(shí)際上這并不完全正確,有一種特殊情況會(huì)導(dǎo)致這個(gè)假設(shè)是錯(cuò)誤的。 a = 2; var a; console.log(a); 大家...
摘要:而作為構(gòu)造函數(shù),需要有個(gè)屬性用來(lái)作為以該構(gòu)造函數(shù)創(chuàng)造的實(shí)例的繼承。 歡迎來(lái)我的博客閱讀:「JavaScript 原型中的哲學(xué)思想」 記得當(dāng)年初試前端的時(shí)候,學(xué)習(xí)JavaScript過(guò)程中,原型問(wèn)題一直讓我疑惑許久,那時(shí)候捧著那本著名的紅皮書(shū),看到有關(guān)原型的講解時(shí),總是心存疑慮。 當(dāng)在JavaScript世界中走過(guò)不少旅程之后,再次萌發(fā)起研究這部分知識(shí)的欲望,翻閱了不少書(shū)籍和資料,才搞懂...
摘要:標(biāo)準(zhǔn)對(duì)象,語(yǔ)義由本規(guī)范定義的對(duì)象。這意味著雖然有,本質(zhì)上依然是構(gòu)造函數(shù),并不能像那樣表演多繼承嵌套類等高難度動(dòng)作。不過(guò)這里的并不是我們所說(shuō)的數(shù)據(jù)類型,而是對(duì)象構(gòu)造函數(shù)。 序 ECMAScript is an object-oriented programming language for performing computations and manipulating computat...
摘要:的隱式原型是母,母是由構(gòu)造函數(shù)構(gòu)造的,但函數(shù)的隱式原型又是。。。。可能是考慮到它也是由構(gòu)造函數(shù)生成的吧,所以返回的值也是。 showImg(https://segmentfault.com/img/bVyLk0); 首先,我們暫且把object類型和function類型分開(kāi)來(lái),因?yàn)?function是一個(gè)特殊的對(duì)象類型,我們這里這是便于區(qū)分,把function類型單獨(dú)拿出來(lái)。順便一提,...
摘要:真正的理解閉包的原理與使用更加透徹綁定的四種規(guī)則機(jī)制你不知道的人稱小黃書(shū),第一次看到這本書(shū)名就想到了一句話你懂得,翻閱后感覺(jué)到很驚艷,分析的很透徹,學(xué)習(xí)起來(lái)也很快,塊級(jí)作用域語(yǔ)句語(yǔ)句相當(dāng)于比較麻煩而且用在對(duì)象上創(chuàng)建的塊作用域僅僅在聲明中有效 真正的理解閉包的原理與使用 更加透徹this綁定的四種規(guī)則機(jī)制 你不知道的JavaScript 人稱小黃書(shū),第一次看到這本書(shū)名 就想到了一句話...
閱讀 3547·2021-09-22 15:50
閱讀 3245·2019-08-30 15:54
閱讀 2757·2019-08-30 14:12
閱讀 3067·2019-08-30 11:22
閱讀 2089·2019-08-29 11:16
閱讀 3584·2019-08-26 13:43
閱讀 1198·2019-08-23 18:33
閱讀 930·2019-08-23 18:32