深入學習js之——閉包#8

MagicalLouis發表於2019-03-23

深入學習js系列是自己階段性成長的見證,希望通過文章的形式更加嚴謹、客觀地梳理js的相關知識,也希望能夠幫助更多的前端開發的朋友解決問題,期待我們的共同進步。

如果覺得本系列不錯,歡迎點贊、評論、轉發,您的支援就是我堅持的最大動力。


開篇

閉包(closure)是 Javascript 語言的一個難點,也是它的特色,很多高階應用都要依靠閉包實現。

要理解閉包,首先必須理解 Javascript 特殊的變數作用域。

變數的作用域無非就是兩種:全域性變數和區域性變數。

定義

MDN 對於閉包的定義為:

閉包是指那些能夠訪問自由變數的函式。

那麼什麼是自由變數呢?

自由變數是指在函式中使用的,但既不是函式的引數也不是函式的區域性變數的變數。

由此,我們可以看出來閉包有兩部分組成:

閉包 = 函式+函式能夠訪問的自由變數。

舉一個例子:

var a = 1;
function foo() {
  console.log(a);
}
foo();
複製程式碼

foo 函式可以訪問變數 a 但是 a 既不是 foo 函式的區域性變數,也不是 foo 函式的引數, 所以 a 就是自由變數。

那麼,函式 foo + foo 函式訪問的自由變數 a 就已經構成了一個閉包……

還真的是這樣!

所以在《JavaScript 權威指南》中就講到:從技術的角度講,所有的 JavaScript 函式都是閉包。

上面所說的是理論角度的閉包,其實還有一種實踐角度的閉包,讓我們看看湯姆大叔翻譯的關於閉包的文章中的定義:

ECMAScript 中閉包指的是:

1、從理論角度:所有的函式,因為它們都在建立的時候就將上層上下文的資料儲存下來了,哪怕是簡單的全域性變數也是如此,因為函式中訪問全域性變數就相當於是在訪問自由變數,這個時候使用最外層的作用域。 2、從實踐角度:以下函式才算是閉包:

  • 1、即使建立它的上下文已經銷燬,它仍然存在(比如,內部函式從父函式中返回)。
  • 2、在程式碼中引用自由變數。

接下來就來聊聊實踐上閉包。

分析

讓我們來先看一個例子:

var scope = "global scope";

function checkscope() {
  var scope = "local scope";
  function f() {
    return scope;
  }
  return f;
}

var foo = checkscope();
foo();
複製程式碼

首先我們要分析一下這段程式碼中執行上下文棧和執行上下文的變化。

這裡給出簡要的執行過程: 1、進入全域性程式碼,建立全域性執行上下文,全域性上下文壓入執行上下文棧。 2、全域性執行上下文初始化。 3、執行 checkscope 函式,建立 checkscope 函式執行上下文,checkscope 執行上下文被壓入執行上下文棧。 4、checkscope 執行上下文初始化,建立變數物件、作用域鏈、this 等。 5、checkscope 函式執行完畢,checkscope 執行上下文從執行上下文棧彈出。 6、執行 f 函式,建立 f 函式的執行上下文,f 執行上下文被壓入執行上下文棧 7、f 執行上下文初始化,建立變數物件、作用域鏈、this 等 8、f 函式執行完畢,f 函式上下文從執行上下文棧中彈出

瞭解到這個過程,我們應該思考一個問題,那就是:

當 f 函式執行的時候,checkscope 函式上下文已經被銷燬了啊(即已經從執行上下文棧中被彈出),怎麼還會讀取到 checkscope 作用域下的 scope 值呢?

我們瞭解了具體的執行過程後,我們知道 f 執行上下文維護了一個作用域鏈:

fContext = {
  Scope: [AO, checkscopeContext.AO, globalContext.VO]
};
複製程式碼

對的!, 就是因為這個作用域鏈,f 函式依然可以讀取到checkscopeContext.AO 的值,說明當 f 函式引用了 checkscopeContext.AO 中的值的時候,即使 checkscopeContext 被銷燬了,但是 JavaScript 依然會讓 checkscopeContext.AO 活在記憶體中,f 函式依然可以通過 f 函式的作用域鏈找到它,正是因為 JavaScript 做到了這一點,從而實現了閉包這個概念。

所以,讓我們再看一遍實踐角度上閉包的定義:

1、即使建立它的上下文已經銷燬,它仍然存在(比如,內部函式從父函式中返回) 2、在程式碼中引用了自由變數

面試必刷題

接下來,看這道刷題必刷。面試必考的閉包題目:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function() {
    console.log(i);
  };
}
data[0]();
data[1]();
data[2]();
複製程式碼

答案都是 3,讓我們分析一下原因:

當執行到 data[0] 函式之前,此時全域性上下文 VO 為:

globalContext = {
  VO:{
    data:[...],
    i:3
  }
}
複製程式碼

當執行 data[0] 函式的時候,data[0]函式的作用域為:

data[0]Context = {
  Scope:[AO.globalContext.VO]
}
複製程式碼

data[0]Context 的 AO 中並沒有 i 的值,所以會從 globalConetxt.VO 中查詢 i 為 3, 所以列印的結果就是 3,data[1]和 data[2]是一樣的道理。

所以讓我們改成閉包看一下

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function(i) {
    return function() {
      console.log(i);
    };
  })(i);
}
data[0]();
data[1]();
data[2]();
複製程式碼

當指向到 data[0]函式之前,此時全域性執行上下文的 VO 為:

globalContext:{
  VO:{
    dataL[...],
    i:3
  }
}
複製程式碼

跟沒改變之前一樣,

當執行 data[0]函式的時候,data[0]函式的作用域鏈發生了改變:

data[0]Context = {
  Scope:[AO,匿名函式Context.VO globalContext.VO]
}
複製程式碼

匿名函式執行上下文的 AO 為:

匿名函式Context:{
  AO:{
    arguments:{
      0:0,
      length:1
    },
    i = 0
  }
}
複製程式碼

data[0]Context 的 AO 並沒有 i 的值,所以會沿著作用域鏈 從 匿名函式 Context.AO中查詢,這個時候就會找 i 為 0,找到了就不會往 globalContext.VO 中查詢了,即使 globalContext.VO也有 i 的值(值為 3),所以列印的結果就是 0,

data[1] 和 data[2] 是一樣的道理。

參考:

《JavaScript深入之閉包》

《學習JavaScript閉包》

深入學習JavaScript系列目錄

歡迎新增我的個人微信討論技術和個體成長。

深入學習js之——閉包#8
歡迎關注我的個人微信公眾號——指尖的宇宙,更多優質思考乾貨

深入學習js之——閉包#8

相關文章