02.老司機(jī)也會(huì)在閉包相關(guān)知識點(diǎn)翻車(1)

閉包是 JavaScript 中最基本也是最重要的概念之一,很多開發(fā)者都對它「了如指掌」??墒情]包又絕對不是一個(gè)單一的概念:它涉及作用域、作用域鏈、執(zhí)行上下文、內(nèi)存管理等多重知識點(diǎn)。 不管是新手還是「老司機(jī)」,經(jīng)常會(huì)出現(xiàn)「我覺得我弄懂了閉包,但是還會(huì)在一些場景翻車」的情況。這一課我們就對這個(gè)話題進(jìn)行梳理,并最后以「應(yīng)試題」來強(qiáng)化理解閉包。

先看一下跟閉包相關(guān)的知識點(diǎn):


圖片

接下來將通過兩課的內(nèi)容來學(xué)習(xí)這個(gè)主題。

基本知識

作用域

作用域其實(shí)就是一套規(guī)則:這個(gè)規(guī)則用于確定在特定場景下如何查找變量。任何語言都有作用域的概念,同一種語言在演進(jìn)過程中也會(huì)不斷完善其作用域規(guī)則。比如,在 JavaScript 中,ES6 出現(xiàn)之前只有函數(shù)作用域和全局作用域之分。

函數(shù)作用域和全局作用域

大家應(yīng)該非常熟悉函數(shù)作用域了:

function foo() {
  var a = 'bar';
  console.log(a);
}

foo(); // bar

執(zhí)行 foo 函數(shù)時(shí),變量 a 在函數(shù) foo 作用域內(nèi),函數(shù)體內(nèi)可以正常訪問,并輸出 bar。

而當(dāng):

var b = 'bar';

function foo() {
  console.log(b);
}

foo(); // bar

執(zhí)行這段代碼時(shí),foo 函數(shù)在自身函數(shù)作用域內(nèi)并未查找到 b 變量,但是它會(huì)繼續(xù)向外擴(kuò)大查找范圍,因此可以在全局作用域中找到變量 b,輸出 bar。

如果我們稍加改動(dòng):

function bar() {
  var b = 'bar';
}

function foo() {
  console.log(b);
}

foo(); 

執(zhí)行這段代碼時(shí),foo 和 bar 分屬于兩個(gè)彼此獨(dú)立的函數(shù)作用域,foo 函數(shù)無法訪問 bar 函數(shù)中定義的變量 b,且其作用域鏈內(nèi)(上層全局作用域中)也不存在相應(yīng)的變量,因此報(bào)錯(cuò):Uncaught ReferenceError: b is not defined。

總結(jié)一下:在 JavaScript 執(zhí)行一段函數(shù)時(shí),遇見變量讀取其值,這時(shí)候會(huì)「就近」先在函數(shù)內(nèi)部找該變量的聲明或者賦值情況。這里涉及「變量聲明方式」以及「變量提升」的知識點(diǎn),我們后面會(huì)涉及到。如果在函數(shù)內(nèi)無法找到該變量,就要跳出函數(shù)作用域,到更上層作用域中查找。這里的「更上層作用域」可能也是一個(gè)函數(shù)作用域,例如:

function bar() {
  var b = 'bar';
  function foo() {
    console.log(b);
  }
  foo();
}

bar(); // bar

在 foo 函數(shù)執(zhí)行時(shí),對于變量 b 的聲明或讀值情況是在其上層函數(shù) bar 作用域中獲取的。

同時(shí)「更上層作用域」也可以順著作用域范圍向外擴(kuò)散,一直找到全局作用域:

var b = 'bar';
function bar() {
  function foo() {
    console.log(b);
  }
  foo();
}

bar(); // bar

我們看到,變量作用域的查找是一個(gè)擴(kuò)散過程,就像各個(gè)環(huán)節(jié)相扣的鏈條,逐次遞進(jìn),這就是作用域鏈說法的由來。

塊級作用域和暫時(shí)性死區(qū)

作用域概念不斷演進(jìn),ES6 增加了 let 和 const 聲明變量的塊級作用域,使得 JavaScript 中作用域范圍更加豐富。塊級作用域,顧名思義,作用域范圍限制在代碼塊中,這個(gè)概念在其他語言里也普遍存在。當(dāng)然這些新特性的添加,也增加了一定的復(fù)雜度,帶來了新的概念,比如暫時(shí)性死區(qū)。這里有必要稍作展開:說到暫時(shí)性死區(qū),還需要從「變量提升」說起,參看以下代碼:

function foo() {
  console.log(bar);
  var bar = 3;
}
foo(); // undefined

會(huì)輸出:undefined,原因是變量 bar 在函數(shù)內(nèi)進(jìn)行了提升。相當(dāng)于:

function foo() {
  var bar;
  console.log(bar);
  bar = 3;
}
foo(); // undefined

但在使用 let 聲明時(shí):

function foo() {
  console.log(bar);
  let bar = 3;
}
foo(); // 報(bào)錯(cuò)

會(huì)報(bào)錯(cuò):Uncaught ReferenceError: bar is not defined。

我們知道使用 let 或 const 聲明變量,會(huì)針對這個(gè)變量形成一個(gè)封閉的塊級作用域,在這個(gè)塊級作用域當(dāng)中,如果在聲明變量前訪問該變量,就會(huì)報(bào) referenceError 錯(cuò)誤;如果在聲明變量后訪問,則可以正常獲取變量值:

function foo() {
  let bar = 3;
  console.log(bar);
}
foo(); // 3

正常輸出 3。因此在相應(yīng)花括號形成的作用域中,存在一個(gè)「死區(qū)」,起始于函數(shù)開頭,終止于相關(guān)變量聲明的一行。在這個(gè)范圍內(nèi)無法訪問 letconst 聲明的變量。這個(gè)「死區(qū)」的專業(yè)名稱為: TDZ(Temporal Dead Zone),相關(guān)語言規(guī)范的介紹讀者可參考 ECMAScript? 2015 Language Specification ,喜歡刨根問底看規(guī)范的讀者可以了解一下。

參考下面圖示,我們加深理解:


圖片

除了自身作用域內(nèi)的 foo3 以外,bar2 函數(shù)可以訪問 foo2、 foo1;但是 bar1 函數(shù)卻無法訪問 bar2 函數(shù)內(nèi)定義的 foo3。

圖片

再啰嗦一遍,bar1 函數(shù) let foo3 = 'foo3' 代碼執(zhí)行前,為「死區(qū)」,訪問變量 foo3 會(huì)報(bào)錯(cuò);該行后即可正常訪問。

注意我在上圖中勾出的暫時(shí)性死區(qū)區(qū)域,這里介紹一個(gè)比較「極端」的情況:函數(shù)的參數(shù)默認(rèn)值設(shè)置也會(huì)受到 TDZ 的影響:

function foo(arg1 = arg2, arg2) {
  console.log(`${arg1} ${arg2}`);
}

// 在上面 foo 函數(shù)中,如果第一個(gè)參數(shù)沒有傳,將會(huì)使用第二個(gè)參數(shù)作為第一個(gè)實(shí)參值。調(diào)用:

foo('arg1', 'arg2') // arg1 arg2

// 返回內(nèi)容正常,但是當(dāng)?shù)谝粋€(gè)參數(shù)缺省時(shí),執(zhí)行 arg1 = arg2 會(huì)當(dāng)作暫時(shí)性死區(qū)處理:
// 因?yàn)槌藟K級作用域以外,函數(shù)參數(shù)默認(rèn)值也會(huì)受到 TDZ 影響。

foo(undefined, 'arg2') // Uncaught ReferenceError: arg2 is not defined

// 這里我再「抖個(gè)機(jī)靈」,看看下面的代碼會(huì)輸出什么?
// 這就涉及到 undefined 和 null 的區(qū)別了。在執(zhí)行 foo(null, 'arg2') 時(shí),
// 不會(huì)認(rèn)為「函數(shù)第一個(gè)參數(shù)缺省」,而會(huì)直接接受 null 作為第一個(gè)參數(shù)值。

foo(null, 'arg2') // null arg2

這個(gè)知識點(diǎn)已經(jīng)不是本課的主題了,具體 undefined 和 null 的區(qū)別我們會(huì)在后續(xù)課程中提到。

既然「已經(jīng)偏題」,那我索性再分析一個(gè)場景,順便引出下面的知識點(diǎn):

function foo(arg1) {
   let arg1
}

foo('arg1')

猜猜將會(huì)輸出什么?

實(shí)際上會(huì)報(bào)錯(cuò):Uncaught SyntaxError: Identifier 'arg1' has already been declared。這同樣跟 TDZ 沒有關(guān)系,而是因?yàn)楹瘮?shù)參數(shù)名會(huì)出現(xiàn)在其「執(zhí)行上下文/作用域」當(dāng)中。

在函數(shù)的第一行,便已經(jīng)聲明了 arg1 這個(gè)變量,函數(shù)體再用 let 聲明,會(huì)報(bào)錯(cuò)(這是 let 聲明變量的特點(diǎn),ES6 基礎(chǔ)內(nèi)容,不再展開),類似:

function foo(arg1) {
   var arg1
   let arg1
}

請看示意圖:


圖片

上面我提到了「執(zhí)行上下文」,我們再看看它究竟是什么。

執(zhí)行上下文和調(diào)用棧

很多讀者可能無法準(zhǔn)確定義執(zhí)行上下文和調(diào)用棧,其實(shí),從我們接觸 JavaScript 開始,這兩個(gè)概念便常伴左右。我們寫出的每一行代碼,每一個(gè)函數(shù)都和它們息息相關(guān),但它們卻是「隱形」的,藏在代碼背后,出現(xiàn)在 JavaScript 引擎里。這一小節(jié),我們來剖析一下這兩個(gè)熟悉但又經(jīng)常被忽視的概念。

執(zhí)行上下文就是當(dāng)前代碼的執(zhí)行環(huán)境/作用域,和前文介紹的作用域鏈相輔相成,但又是完全不同的兩個(gè)概念。直觀上看,執(zhí)行上下文包含了作用域鏈,同時(shí)它們又像是一條河的上下游:有了作用域鏈,才有了執(zhí)行上下文的一部分。

代碼執(zhí)行的兩個(gè)階段

理解這兩個(gè)概念,要從 JavaScript 代碼的執(zhí)行過程說起,這在平時(shí)開發(fā)中并不會(huì)涉及,但對于我們理解 JavaScript 語言和運(yùn)行機(jī)制非常重要,請各位細(xì)心閱讀。JavaScript 執(zhí)行主要分為兩個(gè)階段:

  • 代碼預(yù)編譯階段
  • 代碼執(zhí)行階段

預(yù)編譯階段是前置階段,這個(gè)時(shí)候由編譯器將 JavaScript 代碼編譯成可執(zhí)行的代碼。 注意,這里的預(yù)編譯和傳統(tǒng)的編譯并不一樣,傳統(tǒng)的編譯非常復(fù)雜,涉及分詞、解析、代碼生成等過程 。這里的預(yù)編譯是 JavaScript 中獨(dú)特的概念,雖然 JavaScript 是解釋型語言,編譯一行,執(zhí)行一行。但是在代碼執(zhí)行前,JavaScript 引擎確實(shí)會(huì)做一些「預(yù)先準(zhǔn)備工作」。

執(zhí)行階段主要任務(wù)是執(zhí)行代碼,執(zhí)行上下文在這個(gè)階段全部創(chuàng)建完成。
在通過語法分析,確認(rèn)語法無誤之后,JavaScript 代碼在預(yù)編譯階段對變量的內(nèi)存空間進(jìn)行分配,我們熟悉的變量提升過程便是在此階段完成的。

經(jīng)過預(yù)編譯過程,我們應(yīng)該注意三點(diǎn):

  • 預(yù)編譯階段進(jìn)行變量聲明;
  • 預(yù)編譯階段變量聲明進(jìn)行提升,但是值為 undefined;
  • 預(yù)編譯階段所有非表達(dá)式的函數(shù)聲明進(jìn)行提升。

請看下面這道題目:

function bar() {
  console.log('bar1');
}

var bar = function () {
  console.log('bar2');
};

bar(); // bar2

輸出:bar2,我們調(diào)換順序:

var bar = function () {
  console.log('bar2');
};
function bar() {
  console.log('bar1');
}

bar(); // bar2

仍然輸出:bar2,因?yàn)樵陬A(yù)編譯階段變量 bar 進(jìn)行聲明,但是不會(huì)賦值;函數(shù) bar 則進(jìn)行創(chuàng)建并提升。在代碼執(zhí)行時(shí),變量 bar 才進(jìn)行(表達(dá)式)賦值,值內(nèi)容是函數(shù)體為 console.log('bar2') 的函數(shù),輸出結(jié)果 bar2。

請?jiān)偎伎歼@道題:

foo(10);
function foo(num) {
  console.log(foo);
  foo = num;
  console.log(foo);
  var foo;
}

console.log(foo);
foo = 1;
console.log(foo);

// undefined
// 10
// ? foo (num) {
//    console.log(foo)
//    foo = num     
//    console.log(foo)
//    var foo
// }
// 1

在 foo(10) 執(zhí)行時(shí),函數(shù)體內(nèi)進(jìn)行變量提升后,函數(shù)體內(nèi)第一行輸出 undefined,函數(shù)體內(nèi)第三行輸出 foo。接著運(yùn)行代碼,到了整體第 8 行,console.log(foo) 輸出 foo 函數(shù)內(nèi)容(因?yàn)?foo 函數(shù)內(nèi)的 foo = num,將 num 賦值給的是函數(shù)作用域內(nèi)的 foo 變量。)

結(jié)論 作用域在預(yù)編譯階段確定,但是作用域鏈?zhǔn)窃趫?zhí)行上下文的創(chuàng)建階段完全生成的。因?yàn)楹瘮?shù)在調(diào)用時(shí),才會(huì)開始創(chuàng)建對應(yīng)的執(zhí)行上下文。執(zhí)行上下文包括了:變量對象、作用域鏈以及 this 的指向

如圖所示:


圖片

代碼執(zhí)行的整個(gè)過程說起來就像一條生產(chǎn)流水線。第一道工序是在預(yù)編譯階段創(chuàng)建變量對象(Variable Object),此時(shí)只是創(chuàng)建,而未賦值。到了下一道工序代碼執(zhí)行階段,變量對象轉(zhuǎn)為激活對象(Active Object),即完成 VO → AO。此時(shí),作用域鏈也將被確定,它由當(dāng)前執(zhí)行環(huán)境的變量對象和所有外層已經(jīng)完成的激活對象組成。這道工序保證了變量和函數(shù)的有序訪問,即如果當(dāng)前作用域中未找到變量,則繼續(xù)向上查找直到全局作用域。

這樣的工序在流水線上串成一個(gè)整體,這便是 JavaScript 引擎執(zhí)行機(jī)制的最基本道理。

調(diào)用棧

了解了上面的內(nèi)容,函數(shù)調(diào)用棧便很好理解了。我們在執(zhí)行一個(gè)函數(shù)時(shí),如果這個(gè)函數(shù)又調(diào)用了另外一個(gè)函數(shù),而這個(gè)「另外一個(gè)函數(shù)」也調(diào)用了「另外一個(gè)函數(shù)」,便形成了一系列的調(diào)用棧。如下代碼:

function foo1() {
  foo2();
}

function foo2() {
  foo3();
}

function foo3() {
  foo4();
}

function foo4() {
  console.log('foo4');
}

foo1();

調(diào)用關(guān)系:foo1 → foo2 → foo3 → foo4。這個(gè)過程是 foo1 先入棧,緊接著 foo1 調(diào)用 foo2,foo2入棧,以此類推,foo3、foo4,直到 foo4 執(zhí)行完 —— foo4 先出棧,foo3 再出棧,接著是 foo2 出棧,最后是 foo1 出棧。這個(gè)過程「先進(jìn)后出」(「后進(jìn)先出」),因此稱為調(diào)用棧。

我們故意將 foo4 中的代碼寫錯(cuò):

function foo1() {
 foo2()
}
function foo2() {
 foo3()
}
function foo3() {
 foo4()
}
function foo4() {
 console.lg('foo4')
}
foo1()

得到錯(cuò)誤提示如圖:


圖片

或者在 Chrome 中執(zhí)行代碼,打斷點(diǎn)得到:


圖片

不管哪種方式,我們從中都可以借助 JavaScript 引擎,清晰地看到錯(cuò)誤堆棧信息,也就是函數(shù)調(diào)用棧關(guān)系。

注意 正常來講,在函數(shù)執(zhí)行完畢并出棧時(shí),函數(shù)內(nèi)局部變量在下一個(gè)垃圾回收節(jié)點(diǎn)會(huì)被回收,該函數(shù)對應(yīng)的執(zhí)行上下文將會(huì)被銷毀,這也正是我們在外界無法訪問函數(shù)內(nèi)定義的變量的原因。也就是說,只有在函數(shù)執(zhí)行時(shí),相關(guān)函數(shù)可以訪問該變量,該變量在預(yù)編譯階段進(jìn)行創(chuàng)建,在執(zhí)行階段進(jìn)行激活,在函數(shù)執(zhí)行完畢后,相關(guān)上下文被銷毀。

閉包

介紹了這么多前置概念,終于到了閉包環(huán)節(jié)。

閉包并不是 JavaScript 特有的概念,社區(qū)上對于閉包的定義也并不完全相同。雖然本質(zhì)上表達(dá)的意思相似,但是晦澀且多樣的定義仍然給初學(xué)者帶來了困惑。我自己認(rèn)為比較容易理解的閉包定義為:

函數(shù)嵌套函數(shù)時(shí),內(nèi)層函數(shù)引用了外層函數(shù)作用域下的變量,并且內(nèi)層函數(shù)在全局環(huán)境下可訪問,就形成了閉包。

我們看一個(gè)簡單的代碼示例:

function numGenerator() {
  let num = 1;
  num++;
  return () => {
    console.log(num);
  };
}

var getNum = numGenerator();
getNum();

這個(gè)簡單的閉包例子中,numGenerator 創(chuàng)建了一個(gè)變量 num,返回打印 num 值的匿名函數(shù),這個(gè)函數(shù)引用了變量 num,使得外部可以通過調(diào)用 getNum 方法訪問到變量 num,因此在 numGenerator 執(zhí)行完畢后,即相關(guān)調(diào)用棧出棧后,變量 num 不會(huì)消失,仍然有機(jī)會(huì)被外界訪問。

執(zhí)行代碼,能清晰地看到 JavaScript 引擎的分析:


圖片

num 值被標(biāo)記為 Closure,即閉包變量。

對比前述內(nèi)容,我們知道正常情況下外界是無法訪問函數(shù)內(nèi)部變量的,函數(shù)執(zhí)行完之后,上下文即被銷毀。但是在(外層)函數(shù)中,如果我們返回了另一個(gè)函數(shù),且這個(gè)返回的函數(shù)使用了(外層)函數(shù)內(nèi)的變量,外界因而便能夠通過這個(gè)返回的函數(shù)獲取原(外層)函數(shù)內(nèi)部的變量值。這就是閉包的基本原理。

因此,直觀上來看,閉包這個(gè)概念為 JavaScript 中訪問函數(shù)內(nèi)變量提供了途徑和便利。這樣做的好處很多,比如我們可以利用閉包實(shí)現(xiàn)「模塊化」;再比如,翻看 Redux 源碼的中間件實(shí)現(xiàn)機(jī)制,也會(huì)發(fā)現(xiàn)(函數(shù)式理念)大量運(yùn)用了閉包。這些更加深入的內(nèi)容我們后續(xù)課程都將會(huì)涉及。閉包是前端進(jìn)階必備基礎(chǔ)。后面我們還會(huì)通過做題的方式,幫助讀者深化理解閉包。

內(nèi)存管理

內(nèi)存管理是計(jì)算機(jī)科學(xué)中的概念。不論是什么程序語言,內(nèi)存管理都是指對內(nèi)存生命周期的管理,而內(nèi)存的生命周期無外乎:

  • 分配內(nèi)存空間
  • 讀寫內(nèi)存
  • 釋放內(nèi)存空間

我們用代碼來舉例:

var foo = 'bar' // 在堆內(nèi)存中給變量分配空間
alert(foo)  // 使用內(nèi)存
foo = null // 釋放內(nèi)存空間
內(nèi)存管理基本概念

我們知道內(nèi)存空間可以分為??臻g和堆空間,其中

  • ??臻g:由操作系統(tǒng)自動(dòng)分配釋放,存放函數(shù)的參數(shù)值,局部變量的值等,其操作方式類似于數(shù)據(jù)結(jié)構(gòu)中的棧。
  • 堆空間:一般由開發(fā)者分配釋放,這部分空間就要考慮垃圾回收的問題。

在 JavaScript 中,數(shù)據(jù)類型包括(未包含 ES Next 新數(shù)據(jù)類型):

  • 基本數(shù)據(jù)類型,如 Undefined、Null、Number、Boolean、String 等
  • 引用類型,如 Object、Array、Function 等

一般情況下,基本數(shù)據(jù)類型保存在棧內(nèi)存當(dāng)中,引用類型保存在堆內(nèi)存當(dāng)中。如下代碼:

var a = 11
var b = 10
var c = [1, 2, 3]
var d = { e: 20 }

對應(yīng)內(nèi)存分配圖示:


圖片

對于分配內(nèi)存和讀寫內(nèi)存的行為所有語言都較為一致,但釋放內(nèi)存空間在不同語言之間有差異。例如,JavaScript 依賴宿主瀏覽器的垃圾回收機(jī)制,一般情況下不用程序員操心。但這并不表示萬事大吉,某些情況下依然會(huì)出現(xiàn)內(nèi)存泄漏現(xiàn)象。

內(nèi)存泄漏是指內(nèi)存空間明明已經(jīng)不再被使用,但由于某種原因并沒有被釋放的現(xiàn)象。這是一個(gè)非?!感W(xué)」的概念,因?yàn)閮?nèi)存空間是否還在使用,某種程度上是不可判定問題,或者判定成本很高。內(nèi)存泄漏危害卻非常直觀:它會(huì)直接導(dǎo)致程序運(yùn)行緩慢,甚至崩潰。

內(nèi)存泄漏場景舉例
我們來看幾個(gè)典型引起內(nèi)存泄漏的例子:

var element = document.getElementById("element")
element.mark = "marked"
// 移除 element 節(jié)點(diǎn)
function remove() {
   element.parentNode.removeChild(element)
}

上面的代碼,我們只是把 id 為 element 的節(jié)點(diǎn)移除,但是變量 element 依然存在,該節(jié)點(diǎn)占有的內(nèi)存無法被釋放。

請仔細(xì)參考下圖:


圖片

我們需要在 remove 方法中添加:element = null,這樣更為穩(wěn)妥。

再來看個(gè)示例:

var element = document.getElementById('element')
element.innerHTML = '點(diǎn)擊'

var button = document.getElementById('button')
button.addEventListener('click', function() {
   // ...
})

element.innerHTML = ''

這段代碼執(zhí)行后,因?yàn)?element.innerHTML = '',button 元素已經(jīng)從 DOM 中移除了,但是由于其事件處理句柄還在,所以依然無法被垃圾回收。我們還需要增加 removeEventListener,防止內(nèi)存泄漏。

另一個(gè)示例:

function foo() {
 var name  = 'lucas'
 window.setInterval(function() {
   console.log(name)
 }, 1000)
}

foo()

這段代碼由于 window.setInterval 的存在,導(dǎo)致 name 內(nèi)存空間始終無法被釋放,如果不是業(yè)務(wù)要求的話,一定要記得在合適的時(shí)機(jī)使用 clearInterval 進(jìn)行清理。

瀏覽器垃圾回收
當(dāng)然,除了開發(fā)者主動(dòng)保證以外,大部分的場景瀏覽器都會(huì)依靠:

  • 標(biāo)記清除
  • 引用計(jì)數(shù)

兩種算法來進(jìn)行主動(dòng)垃圾回收。內(nèi)容社區(qū)上有很多好文章介紹這方面的內(nèi)容,我把自己收藏的幾篇不錯(cuò)的跟大家分享一下,這些內(nèi)容偏瀏覽器引擎實(shí)現(xiàn),這里不再過多介紹,感興趣的讀者可以參考下面內(nèi)容:

內(nèi)存泄漏和垃圾回收注意事項(xiàng)
關(guān)于內(nèi)存泄漏和垃圾回收,要在實(shí)戰(zhàn)中分析,不能完全停留在理論層面,畢竟如今瀏覽器千變?nèi)f化且一直在演進(jìn)當(dāng)中。 從以上示例我們可以看出,借助閉包來綁定數(shù)據(jù)變量,可以保護(hù)這些數(shù)據(jù)變量的內(nèi)存塊在閉包存活時(shí),始終不被垃圾回收機(jī)制回收。因此,閉包使用不當(dāng),極可能引發(fā)內(nèi)存泄漏,需要格外注意。以下代碼:

function foo() {
   let value = 123
   function bar() { alert(value) }
   return bar
}

let bar = foo()

這種情況下,變量 value 將會(huì)保存在內(nèi)存中,如果加上:

bar = null

這樣的話,隨著 bar 不再被引用,value 也會(huì)被清除。

結(jié)合瀏覽器引擎的優(yōu)化情況,我們對上述代碼進(jìn)行改動(dòng):

function foo() {
   let value = Math.random()
   function bar() {
       debugger
   }
   return bar
}

let bar = foo()
bar()

在 Chrome 瀏覽器 V8 最新引擎中,執(zhí)行上述代碼。我們在函數(shù) bar 中打斷點(diǎn),會(huì)發(fā)現(xiàn) value 沒有被引用,如下圖:


圖片

而我們在 bar 函數(shù)中加入對 value 的引用:

function foo() {
   let value = Math.random()
   function bar() {
       console.log(value)
       debugger
   }
   return bar
}

let bar = foo()
bar()

會(huì)發(fā)現(xiàn)此時(shí)引擎中存在閉包變量 value 值。如下圖:


圖片

下面我們來看一個(gè)實(shí)戰(zhàn),借助 Chrome devtool,排查發(fā)現(xiàn)內(nèi)存泄漏的場景。

var array = []
function createNodes() {
   let div
   let i = 100
   let frag = document.createDocumentFragment()
   for (; i > 0; i--) {
       div = document.createElement("div")
       div.appendChild(document.createTextNode(i))
       frag.appendChild(div)
   }
   document.body.appendChild(frag)
}
function badCode() {
   array.push([...Array(100000).keys()])
   createNodes()
   setTimeout(badCode, 1000)
}

badCode()

我們遞歸調(diào)用 badCode,這個(gè)函數(shù)每次向 array 數(shù)組中寫入新的由 100000 項(xiàng)從 0 到 1 組成的新數(shù)組,在 badCode函數(shù)使用完全局變量 array 之后,并沒有手動(dòng)釋放內(nèi)存,垃圾回收不會(huì)處理 array,導(dǎo)致內(nèi)存泄漏;同時(shí),badCode函數(shù)調(diào)用 createNodes 函數(shù),每 1s 創(chuàng)建 100 個(gè) div 節(jié)點(diǎn)。

這時(shí)候,打開 Chrome devtool,我們選中 performance 標(biāo)簽,拍下快照得到:


1.png

由此可以發(fā)現(xiàn),JS heap(藍(lán)線)和 Nodes(綠線)線,隨著時(shí)間線一直在上升,并沒有被垃圾回收。因此,可以判定存在較大的內(nèi)存泄漏風(fēng)險(xiǎn)。如果我們不知道有問題的代碼位置,具體如何找出風(fēng)險(xiǎn)點(diǎn),那需要在 Chrome memory 標(biāo)簽中,對 JS heap 中每一項(xiàng),尤其是 size 較大的前幾項(xiàng)展開調(diào)查。如圖:


圖片

圖片

明顯就是我們定義的 array 不對勁了。

這一節(jié)我們分析了涉及閉包知識的基礎(chǔ)概念,介紹了內(nèi)存管理和垃圾回收相關(guān)機(jī)制。下一節(jié)我們將集中學(xué)習(xí)代碼示例,加強(qiáng)理解。

文章來源:https://www.zhihu.com/market/paid_column/1167078439772721152/section/11699471956707

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容