JavaScript閉包是什么及怎么用

這篇“JavaScript閉包是什么及怎么用”文章的知識(shí)點(diǎn)大部分人都不太理解,所以小編給大家總結(jié)了以下內(nèi)容,內(nèi)容詳細(xì),步驟清晰,具有一定的借鑒價(jià)值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“JavaScript閉包是什么及怎么用”文章吧。

我們一直強(qiáng)調(diào)成都做網(wǎng)站、網(wǎng)站設(shè)計(jì)對(duì)于企業(yè)的重要性,如果您也覺得重要,那么就需要我們慎重對(duì)待,選擇一個(gè)安全靠譜的網(wǎng)站建設(shè)公司,企業(yè)網(wǎng)站我們建議是要么不做,要么就做好,讓網(wǎng)站能真正成為企業(yè)發(fā)展過程中的有力推手。專業(yè)網(wǎng)站建設(shè)公司不一定是大公司,成都創(chuàng)新互聯(lián)作為專業(yè)的網(wǎng)絡(luò)公司選擇我們就是放心。

閉包是什么?

對(duì)于一個(gè)知識(shí)點(diǎn)來說,我一直認(rèn)為不論是從什么方面入手,都需要徹底弄懂三個(gè)問題,才算真正了解這個(gè)知識(shí)點(diǎn),然后具體再去實(shí)踐中練習(xí),才能稱得上掌握。這三個(gè)問題就是:

  • 是什么?

  • 為什么要設(shè)計(jì)?

  • 能用在哪?

首先先回答閉包是什么這個(gè)問題。應(yīng)該大多數(shù)人也看過很多與之相關(guān)的文章,很多人也給出了自己的解釋,所以我也先給出自己理解的解釋,那就是: 先有兩個(gè)前置的概念:

  • 閉包是在詞法分析時(shí)就已經(jīng)被確定的, 所以它會(huì)與詞法作用域有關(guān)。

  • 閉包存在的前置條件是需要支持函數(shù)作為一等公民的編程語言,所以它會(huì)與函數(shù)有關(guān)。

所以最終的結(jié)論就是:

  • 閉包首先是一個(gè)結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體的組成部分為  一個(gè)函數(shù) + 該函數(shù)所處的詞法作用域

  • 也就是閉包是由一個(gè)函數(shù)并且該函數(shù)能夠記住聲明自己的詞法作用域所產(chǎn)生的結(jié)構(gòu)體。

  • 在內(nèi)存中理解就是, 當(dāng)一個(gè)函數(shù)被調(diào)用時(shí),它所產(chǎn)生的函數(shù)執(zhí)行上下文里的作用域鏈保存有其父詞法作用域,所以父變量對(duì)象由于存在被引用而不會(huì)銷毀,駐留在內(nèi)存中供其使用。這樣的情況就稱為閉包。

為什么要設(shè)計(jì)出閉包?

對(duì)于為什么設(shè)計(jì)這點(diǎn),僅以我自己粗淺的理解就是由于JavaScript是異步單線程的語言。對(duì)于異步編程來說,最大的問題就是當(dāng)你編寫了函數(shù),而等到它真正調(diào)用的時(shí)機(jī)可能是之后任意的時(shí)間節(jié)點(diǎn)。

這對(duì)于內(nèi)存管理來說是一個(gè)很大的問題,正常同步執(zhí)行的代碼,函數(shù)聲明時(shí)和被調(diào)用時(shí)所需要的數(shù)據(jù)都還存留在內(nèi)存中,可以無障礙的獲取。而異步的代碼,往往聲明該函數(shù)的上下文可能已經(jīng)銷毀,等到在調(diào)用它時(shí),如果內(nèi)存中已經(jīng)把它所需要的一些外部數(shù)據(jù)給清理了,這就是個(gè)很大的問題。

所以JavaScript解決的方案就是讓函數(shù)能夠記得自己之前所能獲取數(shù)據(jù)的范圍,統(tǒng)統(tǒng)都保存在內(nèi)存里,只要該函數(shù)沒有被內(nèi)存回收,它自身以及所能記住的范圍都不會(huì)被銷毀。

這里的所能記住的范圍就是指詞法作用域,就是由于它是靜態(tài)的,所以才需要記住。

這又是JavaScript設(shè)計(jì)作用域?yàn)殪o態(tài)的導(dǎo)致的。 如果是動(dòng)態(tài)作用域,函數(shù)被調(diào)用時(shí)只需要被調(diào)用時(shí)的那個(gè)環(huán)境,就不需要存在記住自身作用域的事了。

所以總結(jié)一下就是:

  • 閉包是為了解決詞法作用域引發(fā)的問題內(nèi)存不好管理異步編程里數(shù)據(jù)獲取所產(chǎn)生的。

經(jīng)典題

由于有非常多的文章都從下面這個(gè)非常經(jīng)典的面試題入手,但似乎都沒有人真正從最底層講解過,所以我就打算將整個(gè)過程梳理一遍,來明白這其中的差異性。

for (var i = 0; i < 3; i++) {  setTimeout(function cb() {    console.log(i);
  }, 1000);
}

基本所有有基礎(chǔ)的人一眼就能看出輸出的是三個(gè)3。

然后讓修改成按順序輸出,通常只需要修改var成let:

for (let i = 0; i < 3; i++) {  setTimeout(function cb() {    console.log(i);
  }, 1000);
}

這樣就成了輸出為0,1,2.并且是同時(shí)間輸出,而不是每間隔一秒輸出一次。

那么問題來了,為什么?

這里可以先不看下面,先寫寫自己的解釋,看看是否跟我寫的一樣。

1. 先來探討變量i是var的情況。

當(dāng)代碼開始執(zhí)行時(shí),此時(shí)執(zhí)行上下文棧和內(nèi)存里的情況是這樣: 其中全局對(duì)象里的變量i和全局執(zhí)行上下文里變量環(huán)境里的變量i是同一個(gè)變量。

JavaScript閉包是什么及怎么用

然后開始進(jìn)行循環(huán), 當(dāng) i = 0時(shí),第一個(gè)定時(shí)器被丟入宏任務(wù)隊(duì)列,關(guān)于宏任務(wù)相關(guān)的內(nèi)容屬于事件循環(huán)范疇,暫時(shí)只需要理解setTimeout會(huì)被丟入隊(duì)列里,等之后執(zhí)行。 此時(shí)在堆內(nèi)存中會(huì)創(chuàng)建它的回調(diào)函數(shù)cb,并且函數(shù)創(chuàng)建時(shí)會(huì)創(chuàng)建[[scope]],在實(shí)際ECMA的規(guī)則中,[[scope]]會(huì)指向該函數(shù)的父作用域,也就是當(dāng)前的全局對(duì)象(作用域是概念上的東西,實(shí)際體現(xiàn)在內(nèi)存中就是保存數(shù)據(jù)的一種結(jié)構(gòu),可能是對(duì)象也可能是其他)。 但是在V8引擎的實(shí)現(xiàn)中,其實(shí)并不會(huì)指向全局對(duì)象,而是去分析該函數(shù)使用了父作用域中的哪些變量,將這些變量存儲(chǔ)到Closure中,然后由scope指向。每個(gè)函數(shù)都有且只有一個(gè)Closure對(duì)象。

JavaScript閉包是什么及怎么用


這里先插入一下關(guān)于Closure對(duì)象可以在Chrome中哪看到的情況: 可以看到,創(chuàng)建bar函數(shù)時(shí),它只有引用了父作用域的name變量,所以在閉包對(duì)象中只會(huì)存儲(chǔ)變量name, 而不會(huì)存在變量age。JavaScript閉包是什么及怎么用


同理之后的 i = 1, 和 i = 2 都是一樣的,最終結(jié)果會(huì)變成:

JavaScript閉包是什么及怎么用

最終因?yàn)?i++導(dǎo)致 i = 3, 循環(huán)結(jié)束,全局代碼執(zhí)行完畢。此時(shí)的結(jié)果為:

然后開始進(jìn)入定時(shí)器回調(diào)函數(shù)執(zhí)行的過程, 開始執(zhí)行第一個(gè)定時(shí)器里的回調(diào)函數(shù),壓入了執(zhí)行上下文棧中,執(zhí)行輸出i, 但是在詞法環(huán)境和變量環(huán)境中找不到這個(gè)變量i,所以去自身[[scope]]向上尋找,在Closure對(duì)象中找到了 i 等于3,輸出結(jié)果3。

JavaScript閉包是什么及怎么用

同理對(duì)于后面兩個(gè)定時(shí)器也是一樣的流程,并且實(shí)際上定時(shí)器開啟的時(shí)間都是在循環(huán)中就立即執(zhí)行的,導(dǎo)致實(shí)際上三個(gè)函數(shù)的定時(shí)1秒時(shí)間是一致的,最終輸出的結(jié)果是幾乎同時(shí)輸出3個(gè)3。而不是每間隔1秒后輸出3, 當(dāng)然這是定時(shí)器相關(guān)的知識(shí)了。

2. 然后探討通過var修改成let之后實(shí)際上變了什么

同樣是剛創(chuàng)建時(shí),所展示的情況為:

JavaScript閉包是什么及怎么用

之后進(jìn)入循環(huán)體,當(dāng)i = 0時(shí):

JavaScript閉包是什么及怎么用

之后進(jìn)入 i = 1時(shí)的情況:

JavaScript閉包是什么及怎么用

最后進(jìn)入到 i = 2的情況,與 i = 1基本類似:

JavaScript閉包是什么及怎么用

最終 i++,變成i值為3,循環(huán)結(jié)束。開啟定時(shí)器工作:

JavaScript閉包是什么及怎么用

當(dāng)執(zhí)行第一個(gè)定時(shí)器的回調(diào)函數(shù)時(shí),創(chuàng)建了函數(shù)執(zhí)行上下文,此時(shí)執(zhí)行輸出語句i時(shí),會(huì)先從自己的詞法環(huán)境里尋找變量i的值,也就是在 record環(huán)境記錄里搜索,但是不存在。因而通過自己外部環(huán)境引用outer找到原先創(chuàng)建的塊級(jí)作用域里 i = 0的情況, 輸出了i值為0的結(jié)果。

對(duì)于之后的定時(shí)器也都是一樣的情況,原先的塊級(jí)作用域由于被回調(diào)函數(shù)所引用到了,因而就產(chǎn)生了閉包的情況,不會(huì)在內(nèi)存中被銷毀,而是一直留著。

等到它們都執(zhí)行完畢后,最終內(nèi)存回收會(huì)將之全部都銷毀。

其實(shí)以上畫的圖并不是很嚴(yán)謹(jǐn),與實(shí)際在內(nèi)存中的表現(xiàn)肯定是有差異的,但是對(duì)于理解閉包在內(nèi)存里的情況還是不影響的。

閉包能用在哪?

首先需要先明確一點(diǎn),那就是在JavaScript中,只要?jiǎng)?chuàng)建了函數(shù),其實(shí)就產(chǎn)生了閉包。這是廣義上的閉包,因?yàn)樵谌肿饔糜蛳侣暶鞯暮瘮?shù),也會(huì)記著全局作用域。而不是只有在函數(shù)內(nèi)部聲明的函數(shù)才叫做閉包。

通常意義上所討論的閉包,是使用了閉包的特性。

1. 函數(shù)作為返回值

let a = 1function outer() {  let a = 2

  function inside() {
    a += 1
    console.log(a)
  }  return inside
}const foo = outer()foo()

此處outer函數(shù)調(diào)用完時(shí),返回了一個(gè)inside函數(shù),在執(zhí)行上下文棧中表示的既是outer函數(shù)執(zhí)行上下文被銷毀,但有一個(gè)返回值是一個(gè)函數(shù)。 該函數(shù)在內(nèi)存中創(chuàng)建了一個(gè)空間,其[[scope]]指向著outer函數(shù)的作用域。因而outer函數(shù)的環(huán)境不會(huì)被銷毀。

當(dāng)foo函數(shù)開始調(diào)用時(shí),調(diào)用的就是inside函數(shù),所以它在執(zhí)行時(shí),先詢問自身作用域是否存在變量a, 不存在則向上詢問自己的父作用域outer,存在變量a且值為2,最終輸出3。

2. 函數(shù)作為參數(shù)

var name = 'xavier'function foo() {  var name = 'parker'
  function bar() {    console.log(name)
  } console.log(name)  return bar
}function baz(fn) {  var name = 'coin'
  fn()
}baz(foo())baz(foo)

對(duì)于第一個(gè)baz函數(shù)調(diào)用,輸出的結(jié)果為兩個(gè)'parker'。 對(duì)于第二個(gè)baz函數(shù)的調(diào)用,輸出為一個(gè)'parker'。

具體的理解其實(shí)跟上面一致,只要函數(shù)被其他函數(shù)調(diào)用,都會(huì)存在閉包。

3. 私有屬性

閉包可以實(shí)現(xiàn)對(duì)于一些屬性的隱藏,外部只能獲取到屬性,但是無法對(duì)屬性進(jìn)行操作。

function foo(name) {  let _name = name  return {    get: function() {      return _name
    }
  }
}let obj = foo('xavier')
obj.get()

4. 高階函數(shù),科里化,節(jié)流防抖等

對(duì)于一些需要存在狀態(tài)的函數(shù),都是使用到了閉包的特性。

// 節(jié)流function throttle(fn, timeout) {  let timer = null
  return function (...arg) {    if(timer) return
    timer = setTimeout(() => {
    fn.apply(this, arg)
    timer = null
    }, timeout)
  }
}// 防抖function debounce(fn, timeout){  let timer = null
  return function(...arg){    clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, arg)
    }, timeout)
  }
}

5. 模塊化

在沒有模塊之前,對(duì)于不同地方聲明的變量,可能會(huì)產(chǎn)生沖突。而閉包能夠創(chuàng)造出一個(gè)封閉的私有空間,為模塊化提供了可能性。 可以使用IIFE+閉包實(shí)現(xiàn)模塊。

var moduleA = (function (global, doc) {  var methodA = function() {};  var dataA = {};  return {    methodA: methodA,    dataA: dataA
  };
})(this, document);

以上就是關(guān)于“JavaScript閉包是什么及怎么用”這篇文章的內(nèi)容,相信大家都有了一定的了解,希望小編分享的內(nèi)容對(duì)大家有幫助,若想了解更多相關(guān)的知識(shí)內(nèi)容,請(qǐng)關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。

本文題目:JavaScript閉包是什么及怎么用
文章起源:http://muchs.cn/article44/jpidhe.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供靜態(tài)網(wǎng)站、企業(yè)網(wǎng)站制作自適應(yīng)網(wǎng)站、網(wǎng)頁設(shè)計(jì)公司、面包屑導(dǎo)航、外貿(mào)網(wǎng)站建設(shè)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)

營(yíng)銷型網(wǎng)站建設(shè)