溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

淺談JavaScript閉包

發(fā)布時間:2020-08-31 11:39:35 來源:腳本之家 閱讀:123 作者:豐寸 欄目:web開發(fā)

最近朋友面試被問到了 JS 閉包的問題,本人一時語塞,想起了袁華的一句話:“這道題太難了,我不會做,不會做?。 ?。

JS 閉包屬于面向?qū)ο蟮囊粋€重要知識點,特此本人又開始了一段說走就走的旅程。

閉包就是外層函數(shù)的作用域(AO)對象被內(nèi)層函數(shù)所引用,無法被釋放。

上面那句話聽起來可能不是很理解,本人在之前寫過一篇Python 閉包小記》的關(guān)于 Python 閉包的一些知識的文章,里面寫了百度百科對于閉包的理解,雖然由于才疏學淺大部分都是引用的他人的知識架構(gòu),但語言這種東西都是相通的,我們不需要去記那些晦澀的名詞,對于閉包,作為初學者我們只需知道:

函數(shù)作為返回值,函數(shù)作為參數(shù)傳遞。就可以將其理解為閉包。

話不多說,先上個代碼緩和一下尷尬的氣氛:

function outer() {
  var max = 10;
  function inner(num) {
    if (num > max) {
      console.log(num)
    }
  }
  return inner;
}
var foo = outer();
foo(20); // 20

上面代碼滿足函數(shù)作為返回值的條件,所以是一個閉包函數(shù)。

根據(jù) JS 函數(shù)的執(zhí)行機制,先執(zhí)行第 10 行的 foo 代碼,在函數(shù)執(zhí)行完之后會被 JS 的垃圾回收機制將 outer 函數(shù)回收,但是在執(zhí)行到第 3 行的時候我們發(fā)現(xiàn) outer 函數(shù)內(nèi)部又出現(xiàn)了一個 inner 函數(shù),且 inner 函數(shù)里引用著 outer 函數(shù)的 max = 10; 的變量,這就無法被回收并且留在了內(nèi)存里,當執(zhí)行到第 11 行時由于 outer 函數(shù)內(nèi)的 max = 10; 被留在內(nèi)存中,所以會被 inner 函數(shù)調(diào)用,并滿足 if 條件判斷,所以輸出 20;

以上我們實現(xiàn)了一個簡單的閉包函數(shù),但是卻產(chǎn)生了一個問題,那就是無法被釋放的對象留在了內(nèi)存當中,造成了不必要的內(nèi)存開銷。

再看如下代碼:

var max = 10,
  foo = function (num) {
    if (num > max) {
      console.log(num);
    }
  };
(function (bar) {
  var max = 100;
  bar(20)
})(foo);  // 20

上面代碼滿足函數(shù)作為參數(shù)傳遞的條件,所以是一個閉包函數(shù)。

函數(shù) foo 作為一個參數(shù)被傳入函數(shù)中,賦值個 bar 參數(shù),當執(zhí)行到 bar() 函數(shù)時,函數(shù)內(nèi)部的 max 并不是 100,而是 10,這似乎匪夷所思。我們暫且將 7 — 10 行的函數(shù)叫 “父作用域”,其余叫“全局作用域”,當執(zhí)行到 bar(20) 時,函數(shù)去執(zhí)行第 2 行的代碼,此時 foo 函數(shù)內(nèi)部的 max 要去取值,而 max = 10; 正好在他所在的 “全局作用域” 內(nèi),所以會取 max = 10; 的值而不是 max = 100; 的值。由此可見,取值時要去創(chuàng)造這個函數(shù)的作用域內(nèi)取值,而不是所謂的 “父作用域” 或者離函數(shù)近的地方取值。

我們再來看一段代碼:

var num = 20;
function outer() {
  var max = 10;
  function inner() {
    if (num > max) {
      console.log(num);
    }
  }
  return inner;
}
var foo = outer(),
  num = 30;
foo(); //30

上面的代碼在看完上面的解釋后可以得知它是一個閉包函數(shù),且定義了一個全局變量 num,最初定義為 num = 20,當代碼執(zhí)行到第 11 行時去調(diào)用執(zhí)行第 2 行,待第 11 行執(zhí)行完畢后執(zhí)行第 12 行,此時將全局的 num = 20; 變?yōu)榱?num = 30; 再執(zhí)行第 13 行,此時執(zhí)行時調(diào)用 inner 函數(shù)時,從輸出結(jié)果我們可以看出調(diào)用的 num 為之后賦值的 30,

由此可見全局的 num 變量被污染了。

我們再來看下一段代碼:

function outer() {
  var max = 10;
  function inner(num) {
    if (num > max) {
      console.log(num);
    }
  }
  return inner;
}
var foo = outer(),
  max = 100;
foo(20);  //20

上面的代碼中當函數(shù)執(zhí)行時,先執(zhí)行第 10 行,然后調(diào)用執(zhí)行第 1 行的函數(shù),此時將 max 賦值為 10,但需要注意的是此時的 max = 10;并不是在全局作用域內(nèi),而是在 outer() 函數(shù)的作用域內(nèi),執(zhí)行完第 10 行再執(zhí)行第 11 行,此時將 max 賦值為 100,但需要注意的是此時的 max = 100;是在全局作用域內(nèi)。所以在執(zhí)行到第 12 行代碼的時候調(diào)用執(zhí)行 inner() 函數(shù)并將參數(shù) 20 傳入,輸出結(jié)果為 20,由此可見outer() 函數(shù)作用域內(nèi)的對象 num 并沒有被全局的對象 num 所污染。

由以上四段代碼我們初步了解了一些閉包的基本特征,但是由于才疏學淺,怕總結(jié)的不夠全面,這時突然想到了東東大神的筆記,于是上網(wǎng)搜到了一些,下面就將其再歸納總結(jié)一下。

閉包:既重用一個變量,又保護變量不被污染的一種機制。

為什么使用閉包:

全局變量和局部變量都具有不可兼得的優(yōu)缺點。

全局變量:

  1. 優(yōu): 可重用,
  2. 缺: 易被污染。

局部變量:

  1. 優(yōu): 僅函數(shù)內(nèi)可用,不會被污染
  2. 缺: 不可重用!

何時使用:

只要既重用一個變量,又保護變量不被污染時。

如何使用:

  1. 1. 用外層函數(shù)包裹要保護的變量和內(nèi)層函數(shù)。
  2. 2. 外層函數(shù)將內(nèi)層函數(shù)返回到外部。
  3. 3. 調(diào)用外層函數(shù),獲得內(nèi)層函數(shù)的對象,保存在外部的變量中——形成了閉包。

閉包形成的原因:

外層函數(shù)調(diào)用后,外層函數(shù)的函數(shù)作用域(AO)對象無法釋放,被內(nèi)層函數(shù)引用著。

閉包的缺點:

  1. 比普通函數(shù)占用更多的內(nèi)存。
  2. 解決:閉包不在使用時,要及時釋放。
  3. 將引用內(nèi)層函數(shù)對象的變量賦值為null。

結(jié)合上面舉的四段代碼栗子和東東的筆記,我們已經(jīng)對閉包有了一個形象的認識,但是要到達全面理解的程度,只能說革命尚未成功,同志仍需努力。

令人可喜的是在網(wǎng)上又查到了東東對于閉包更形象的圖形講解,看完之后相信大家對閉包會有更加深刻的理解。

先來一段代碼緩和一下字多的尷尬:

//1. 用外層函數(shù)包裹要保護的變量和內(nèi)層函數(shù)
function outer() {
  var i = 1;
  //2. 外層函數(shù)返回內(nèi)層函數(shù)對象到外部
  return function () {
    console.log(i++);
    
  }
}
//3. 調(diào)用外層函數(shù)獲得內(nèi)層函數(shù)對象
var getNum = outer(); //getNum:function(){ console.log(i++); }
getNum();//1
getNum();//2
i = 1;
getNum();//3
getNum();//4

上面的代碼是定義了一個 outer() 外層函數(shù),外層函數(shù)的作用域內(nèi)定義了 i = 1;的變量,內(nèi)部返回了一個函數(shù),這就形成了閉包。當代碼執(zhí)行到第 10 行,其實就返回了一個 outer() 函數(shù)的內(nèi)部函數(shù),執(zhí)行一次 getNum(),由于打印的是 i++ ,所以輸出結(jié)果為 1,(注:如果打印的是 ++i,輸出結(jié)果為 2 )。再執(zhí)行一次 getNum(),由于之前 i 已經(jīng)執(zhí)行過一次 i++,所以此次執(zhí)行結(jié)果為 2,再在全局設(shè)置 i = 1,再次執(zhí)行 getNum() 兩次,執(zhí)行結(jié)果分別為 3 和 4,說明全局設(shè)置的 i = 1,并沒有覆蓋 outer() 函數(shù)作用域內(nèi)的 i 值,outer() 函數(shù)內(nèi)的 i 值被很好的保護起來并得到了重用。

我們來看看東東對上面代碼的圖形化分析:

淺談JavaScript閉包

如上圖:在 JavaScript 中有一個執(zhí)行環(huán)境棧(ECS)概念,注:ECS = 局部EC + 全局EC,所有的函數(shù)都要通過進棧、出棧來執(zhí)行,執(zhí)行環(huán)境棧中有一個自帶的 main() 函數(shù)的全局EC 指向全局的 window 作用域,它會指向全局的 window 對象,代碼運行到紅線部分的時候,執(zhí)行環(huán)境棧中僅有一個全局執(zhí)行環(huán)境 window,此時 window 中有兩個全局變量(標識符):outer 、getNum,其中 outer() 函數(shù)開辟了一塊內(nèi)存用于存儲所執(zhí)行的方法,并且通過 scope 記住它的父級。

淺談JavaScript閉包

如上圖:當執(zhí)行 outer() 函數(shù)時,outer() 相當于局部EC 進入執(zhí)行環(huán)境棧,此時 outer() 會開辟一塊屬于自己的作用域(AO),里面定義了 i = 1,的環(huán)境變量。 由于 window 中引用著 i 對象,所以 outer 的 AO 會指向 window,同時 getNum 會調(diào)用 outer() 函數(shù)并返回一個方法,所以會開辟一塊內(nèi)存用于存儲所執(zhí)行的方法,該方法中又有 i 變量指向 outer 的 AO,綠色線三方互相牽連。

淺談JavaScript閉包

如上圖:當執(zhí)行環(huán)境棧中的 outer() 函數(shù)執(zhí)行完出棧時,理論上 outer 的 AO,即藍色框應該被垃圾回收機制所回收,但是由于閉包作用,這塊就被留了下來,閉包至此形成。

淺談JavaScript閉包

如上圖:當 outer() 函數(shù)出棧,getNum() 函數(shù)進棧,getNum 開辟屬于自己的作用域(AO),且執(zhí)行了一次 i++ 。此時輸出結(jié)果為 1。

淺談JavaScript閉包

如上圖:當 getNum() 函數(shù)出棧時,自己多開辟的作用域被回收,但是 outer 的作用域由于閉包作用依然留在內(nèi)存中,且變?yōu)榱?i = 2。

淺談JavaScript閉包

如上圖:再次執(zhí)行 getNum() 函數(shù),相當于 getNum() 函數(shù)再次入棧出棧,原來由于閉包作用保留的 i = 2 再次做 ++ 運算。

淺談JavaScript閉包

如上圖:再往下執(zhí)行 i = 1,即在全局 window 當中添加了 i 對象。此時 outer 作用域內(nèi)的 i 由于上一次的 ++ 變?yōu)榱?3。

淺談JavaScript閉包

如上圖:第三次執(zhí)行 getNum() 函數(shù),此時大家應該懂得該怎么執(zhí)行了吧,getNum() 并不會去全局的 window 中去取 i = 1 使用,而是去所創(chuàng)造它的作用域去值,即 i = 3 做 ++ 運算。

至此閉包的運行流程就全部介紹完了,大家是不是對于閉包有了一個比較清晰的了解了。

別急,還差那么一點點,那就是主動釋放閉包所產(chǎn)生的內(nèi)存。如下

//1. 用外層函數(shù)包裹要保護的變量和內(nèi)層函數(shù)
function outer() {
  var i = 1;
  //2. 外層函數(shù)返回內(nèi)層函數(shù)對象到外部
  return function () {
    console.log(i++);
    i = null;
  }
}
//3. 調(diào)用外層函數(shù)獲得內(nèi)層函數(shù)對象
var getNum = outer(); //getNum:function(){ console.log(i++); }
getNum(); //1
getNum(); //0
i = 1;
getNum(); //0
getNum(); //0

在執(zhí)行完第一次 getNum() 函數(shù)時我們就將 i 變量設(shè)為 null,再次執(zhí)行 getNum() 函數(shù)時發(fā)現(xiàn)所得結(jié)果已經(jīng)變?yōu)?0 了,說明 outer() 函數(shù)內(nèi)的 i 變量內(nèi)存已經(jīng)被釋放了!??!

至此 JavaScript 閉包的全部內(nèi)容就講解完畢了,以上內(nèi)容如有紕漏請各位大神批評指正。

好記性不如爛筆頭,特此記錄,與君共勉!

以上所述是小編給大家介紹的JavaScript閉包詳解整合,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對億速云網(wǎng)站的支持!

向AI問一下細節(jié)
推薦閱讀:
  1. Python 閉包
  2. lua 閉包

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI