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