摘要:曾幾何時(shí),閉包好像就是一個(gè)十分難以捉摸透的東西,看了很多文章,對(duì)閉包都各有說法,以致讓我十分暈,什么內(nèi)部變量外部變量的,而且大多數(shù)都只描述一個(gè)過程,沒有給閉包的定義,最后,舉幾個(gè)例子,告訴你這就是閉包。
前提曾幾何時(shí),閉包好像就是一個(gè)十分難以捉摸透的東西,看了很多文章,對(duì)閉包都各有說法,以致讓我十分暈,什么內(nèi)部變量、外部變量的,而且大多數(shù)都只描述一個(gè)過程,沒有給閉包的定義,最后,舉幾個(gè)例子,告訴你這就是閉包。于是乎,我從來都是帶有疑問使用閉包的:閉包是指作用域,還是指函數(shù),還是指訪問外部變量的過程?還有外部變量有多外面?直到最近的學(xué)習(xí),我才漸漸清晰......
首先,我們需要知道 JavaScript 里面的函數(shù)會(huì)創(chuàng)建內(nèi)部詞法作用域,是的,JavaScript 是詞法作用域,也就是說作用域與作用域的層級(jí)關(guān)系在你書寫的時(shí)候就已經(jīng)確定了,而不是調(diào)用的時(shí)候,調(diào)用的時(shí)候確定的稱為動(dòng)態(tài)作用域,由于不是本篇文章的重點(diǎn),就不再詳細(xì)解釋了,舉兩個(gè)例子自己領(lǐng)悟:
var name = "fruit" function apple () { console.log(name) } function orange () { var name = "orange" apple() } orange() // fruit
由于 JavaScript 是詞法作用域,所以 apple 函數(shù)的局部作用域的上層作用域是全局作用域,從書寫的位置就看出來了。假設(shè) JavaScript 是動(dòng)態(tài)作用域,就要看函數(shù)的調(diào)用順序了,由于 apple 是在 orange 中調(diào)用的,所以 apple 的上層作用域是 orange 的局部作用域,那樣的話會(huì)輸出 orange!
這樣的話,就制定了一套作用域訪問的規(guī)則,這也是會(huì)有閉包的原因之一!
什么是閉包?函數(shù)記住并訪問其所在的詞法作用域,叫做閉包現(xiàn)象,而此時(shí)函數(shù)對(duì)作用域的引用叫做閉包。
當(dāng)我看到這句話的時(shí)候,淚流滿面,國外的作者就是一語道破真相。簡單的說,閉包就是引用,對(duì)誰的引用呢,對(duì)作用域的引用,只不過這種引用是有條件的——首先要記住作用域,然后再訪問作用域!
什么叫記住作用域?首先,我們都知道,在 JavaScript 里面,如果函數(shù)被調(diào)用過了,并且以后不會(huì)被用到,那么垃圾回收機(jī)制就會(huì)銷毀由函數(shù)創(chuàng)建的作用域,我們還知道,對(duì)象(函數(shù)也是對(duì)象)的傳遞屬于傳引用,也就是類似于C語言里面的指針,并不會(huì)把真正的值拷貝給變量,而是把對(duì)象所在的位置傳遞給變量,所以,當(dāng)函數(shù)被傳引用到一個(gè)還未銷毀的作用域的某個(gè)變量,由于變量存在,所以函數(shù)得存在,又因?yàn)楹瘮?shù)的存在依賴于函數(shù)所在的詞法作用域,所以函數(shù)所在的詞法作用域也得存在,這樣一來,就記住了該詞法作用域。也就解釋了該節(jié)的標(biāo)題!下面舉個(gè)例子說明一下:
// 沒有閉包現(xiàn)象的時(shí)候 function apple () { var count = 0 function output () { console.log(count) } fruit(output) } function fruit (arg) { console.log("fruit") } apple() // fruit
當(dāng)我們?cè)谡{(diào)用 apple 的時(shí)候,本來 apple 在執(zhí)行完畢之后 apple 的局部作用域就應(yīng)該被銷毀,但是由于 fruit(output) 將 output 傳引用給了 arg,所以在 fruit 執(zhí)行的這段時(shí)間內(nèi),arg 肯定是存在的,被引用的函數(shù) output 也得存在,而 output 依賴的 apple 函數(shù)產(chǎn)生的局部作用域也得存在,這就是所謂的“記住”,把作用域給記住了!
但是,上面的例子是閉包現(xiàn)象嗎?不是,因?yàn)楹瘮?shù) output 內(nèi)部并沒有訪問記住的詞法作用域的變量!在執(zhí)行 fruit(output) 的過程中,只發(fā)生了 arg = output 的傳引用賦值,而這個(gè)過程,只是把二者關(guān)聯(lián)起來了,并沒有去取 arg 引用的對(duì)象的值,所以實(shí)際上也并沒有訪問 output 所在的詞法作用域!
記住并訪問上面的代碼,稍微修改一下就會(huì)產(chǎn)生閉包現(xiàn)象了:
function apple () { var count = 0 function output () { console.log(count) } fruit(output) } function fruit (arg) { arg() } apple() // 0
現(xiàn)在,調(diào)用 fruit 時(shí),apple 的局部作用域處于“記住”的狀態(tài),這時(shí)候, fruit 內(nèi)部調(diào)用了 arg(),因?yàn)閭饕茫瑢?shí)際上訪問并執(zhí)行了 apple 局部作用域的 output,不僅僅是這樣,output 內(nèi)部還訪問了 count 變量,這兩次對(duì) apple 局部作用域的引用都是閉包!
所以,之所以說所有回調(diào)函數(shù)的調(diào)用都會(huì)產(chǎn)生閉包現(xiàn)象,也是因?yàn)檫@個(gè)回調(diào)函數(shù)被傳給了另外一個(gè)函數(shù)的參數(shù),所以在另外一個(gè)函數(shù)的作用域消失之前,回調(diào)函數(shù)所在的詞法作用域都被記住了,由于回調(diào)函數(shù)一定會(huì)被執(zhí)行,所以回調(diào)函數(shù)所在的詞法作用域至少被訪問了一次,也就是至少訪問回調(diào)函數(shù)本身,而這個(gè)對(duì)作用域的引用就是閉包。
閉包的作用根據(jù)上面的講解,估計(jì)你自己都能倒背如流了:
記住了函數(shù)所在的詞法作用域,使其不被銷毀;
能夠訪問函數(shù)所在詞法作用域的變量;
創(chuàng)建模塊(設(shè)計(jì)私有變量、公有函數(shù)等等)
還有很多,就不一一說了,下面就是利用閉包來解決一個(gè)常見的問題:
for (var i = 0; i < 5; i++) { // 為了方便說明,給函數(shù)起名叫 apple setTimeout(function apple () { console.log(i) // 5 個(gè) 5 }, 0) }
首先讀者們先思考一個(gè)問題,這會(huì)產(chǎn)生閉包嗎?
其實(shí),上面也也會(huì)產(chǎn)生閉包,只不過 apple 記住并訪問的是全局作用域,為什么呢?因?yàn)榛卣{(diào)函數(shù)被當(dāng)做 setTimeout 的參數(shù)傳引用過去了,假設(shè) setTimeout 實(shí)現(xiàn)如下
var setTimeout = function (callback, delay) { // 延遲 callback() }
看到?jīng)],因?yàn)?setTimeout 屬于異步函數(shù),所以會(huì)等到 JS 執(zhí)行完畢之后再調(diào)用 callback,所以這段時(shí)間 callback 一直存在,所以函數(shù) apple 也一直存在,所以全局作用域并不會(huì)等 JavaScript 執(zhí)行完畢后就銷毀(函數(shù) apple 屬于全局作用域的),這時(shí)候循環(huán)早結(jié)束了,所以 i 也變成了 5,于是乎,這個(gè)時(shí)候 apple 對(duì)全局作用域的引用稱為閉包!
上面也說了回調(diào)函數(shù)調(diào)用都會(huì)產(chǎn)生閉包,這里就當(dāng)舉例說明一下!
那么怎么解決以上問題呢,很簡單,讓回調(diào)函數(shù)記住不同的作用域就行了!
for (var i = 0; i < 5; i++) { // 為了方便說明,給函數(shù)起名叫 apple (function baz (i) { setTimeout(function apple () { console.log(i) }, 0) })(i) // 0 1 2 3 4 }
上面用立即執(zhí)行函數(shù)解決了問題,因?yàn)楹瘮?shù)有局部作用域,所以調(diào)用 5 次函數(shù)會(huì)產(chǎn)生 5 個(gè)局部作用域,每個(gè)作用域的 i 由各次循環(huán)的 i 傳遞賦值,而每個(gè)作用域內(nèi)都存在 apple ,都記住了各自的作用域,也就取到了不同的 i!
不過通常來說,閉包都是按以下方式產(chǎn)生:
function apple () { var name = "apple" var output = function () { console.log(name) } return output } var out = apple() out() // apple
上述將函數(shù)傳引用給了全局作用域的變量,顯然,閉包(對(duì) apple 作用域的引用)在全局作用域都存在的情況下都可能發(fā)生,而且后面也執(zhí)行了 out()!
更常見的寫法是下面這種:
function Apple () { var name = "apple" var output = function () { console.log(name) } var setName = function (arg) { name = arg } return { output: output, setName: setName } } var apple = Apple() apple.output() // apple apple.setName("Apple") apple.output() // Apple
這就是模塊的一個(gè)例子,name 通常被稱為私有變量!
結(jié)語閉包沒什么了不起的,這是被人玩的過于玄乎,其實(shí)這是人們很自然的想法:我在別的地方調(diào)用函數(shù),總得保持函數(shù)正常運(yùn)行吧!“閉包”這種機(jī)制很輕松的幫你解決了這個(gè)問題,我們不必搞懂閉包是什么也經(jīng)常在實(shí)現(xiàn)它(如果這句話寫在前面,會(huì)不會(huì)很多人都不看了,哈哈),這是語言設(shè)計(jì)者的過人之處,但是,你不搞懂它,總被人質(zhì)疑:你不懂閉包吧!實(shí)際上,我們都實(shí)現(xiàn)了很多次閉包,所以,你把內(nèi)部機(jī)制詳細(xì)搞清楚了,就不會(huì)再害怕別人的質(zhì)疑了,哈哈!當(dāng)然,如果你喜歡鉆研,更有必要了解其中的機(jī)制了,體會(huì)到尋找語言設(shè)計(jì)者設(shè)計(jì)思路的快感!
最后,再總結(jié)一下:函數(shù)記住并訪問其所在的詞法作用域,叫做閉包現(xiàn)象,而此時(shí)函數(shù)對(duì)作用域的引用叫做閉包。
最后的最后,再強(qiáng)調(diào)一下:閉包就是引用!
更多文章請(qǐng)看我的個(gè)人博客
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/80778.html
摘要:大名鼎鼎的閉包面試必問。閉包的作用是什么。看到閉包在哪了嗎閉包到底是什么五年前,我也被這個(gè)問題困擾,于是去搜了并總結(jié)下來。關(guān)于閉包的謠言閉包會(huì)造成內(nèi)存泄露錯(cuò)。閉包里面的變量明明就是我們需要的變量,憑什么說是內(nèi)存泄露這個(gè)謠言是如何來的因?yàn)椤? 本文為饑人谷講師方方原創(chuàng)文章,首發(fā)于 前端學(xué)習(xí)指南。 大名鼎鼎的閉包!面試必問。請(qǐng)用自己的話簡述 什么是「閉包」。 「閉包」的作用是什么。 首先...
摘要:閉包反正看完我就懂了想要好好的理解閉包,你得首先理解作用域。其實(shí)這個(gè)閉包的產(chǎn)生過程可以理解為在里面的匿名函數(shù)定義時(shí)正處于懷孕階段,到外面調(diào)用時(shí),娃就出生了,娃就是閉包啦。閉包改變了變量的生命周期,變量將得到永生。 閉包?反正看完我就懂了 想要好好的理解閉包,你得首先理解作用域。別說了,趕緊去看作用域吧,?,這世界就是如此殘酷。好,言歸正傳,我們是來學(xué)習(xí)閉包的。O(∩_∩)O 什么是閉包...
摘要:所以形式參數(shù)是本地的,不是外部的或者全局的。這叫做函數(shù)聲明,函數(shù)聲明會(huì)連通命名和函數(shù)體一起被提升至作用域頂部。這叫做函數(shù)表達(dá)式,函數(shù)表達(dá)式只有命名會(huì)被提升,定義的函數(shù)體則不會(huì)。 Scoping & Hoisting var a = 1; function foo() { if (!a) { var a = 2; } alert(a); }; ...
摘要:在中,有四種方式可以讓命名進(jìn)入到作用域中按優(yōu)先級(jí)語言定義的命名比如或者,它們?cè)谒凶饔糜騼?nèi)都有效且優(yōu)先級(jí)最高,所以在任何地方你都不能把變量命名為之類的,這樣是沒有意義的形式參數(shù)函數(shù)定義時(shí)聲明的形式參數(shù)會(huì)作為變量被至該函數(shù)的作用域內(nèi)。 Scoping & Hoisting 例: var a = 1; function foo() { if (!a) { var ...
摘要:瀏覽器的主要組成包括有調(diào)用堆棧,事件循環(huán),任務(wù)隊(duì)列和。好了,現(xiàn)在有了前面這些知識(shí),我們可以看一下這道題的講解過程實(shí)現(xiàn)步驟調(diào)用會(huì)將函數(shù)放入調(diào)用堆棧。由于調(diào)用堆棧是空的,事件循環(huán)將選擇回調(diào)并將其推入調(diào)用堆棧進(jìn)行處理。進(jìn)程再次重復(fù),堆棧不會(huì)溢出。 JavaScript是前端開發(fā)中非常重要的一門語言,瀏覽器是他主要運(yùn)行的地方。JavaScript是一個(gè)非常有意思的語言,但是他有很多一些概念,大...
閱讀 3579·2021-11-04 16:06
閱讀 3573·2021-09-09 11:56
閱讀 842·2021-09-01 11:39
閱讀 893·2019-08-29 15:28
閱讀 2289·2019-08-29 15:18
閱讀 823·2019-08-29 13:26
閱讀 3327·2019-08-29 13:22
閱讀 1039·2019-08-29 12:18