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

深刻學習js系列是本身階段性成長的見證,但願經過文章的形式更加嚴謹、客觀地梳理js的相關知識,也但願可以幫助更多的前端開發的朋友解決問題,期待咱們的共同進步。javascript

若是以爲本系列不錯,歡迎點贊、評論、轉發,您的支持就是我堅持的最大動力。html


開篇

閉包(closure)是 Javascript 語言的一個難點,也是它的特點,不少高級應用都要依靠閉包實現。前端

要理解閉包,首先必須理解 Javascript 特殊的變量做用域。java

變量的做用域無非就是兩種:全局變量和局部變量。git

定義

MDN 對於閉包的定義爲:github

閉包是指那些可以訪問自由變量的函數。面試

那麼什麼是自由變量呢?數組

自由變量是指在函數中使用的,但既不是函數的參數也不是函數的局部變量的變量。微信

由此,咱們能夠看出來閉包有兩部分組成:閉包

閉包 = 函數+函數可以訪問的自由變量。

舉一個例子:

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

foo 函數能夠訪問變量 a 可是 a 既不是 foo 函數的局部變量,也不是 foo 函數的參數, 因此 a 就是自由變量。

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

還真的是這樣!

因此在《JavaScript 權威指南》中就講到:從技術的角度講,全部的 JavaScript 函數都是閉包。

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

ECMAScript 中閉包指的是:

一、從理論角度:全部的函數,由於它們都在建立的時候就將上層上下文的數據保存下來了,哪怕是簡單的全局變量也是如此,由於函數中訪問全局變量就至關因而在訪問自由變量,這個時候使用最外層的做用域。 二、從實踐角度:如下函數纔算是閉包:

  • 一、即便建立它的上下文已經銷燬,它仍然存在(好比,內部函數從父函數中返回)。
  • 二、在代碼中引用自由變量。

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

分析

讓咱們來先看一個例子:

var scope = "global scope";

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

var foo = checkscope();
foo();
複製代碼

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

這裏給出簡要的執行過程:

一、進入全局代碼,建立全局執行上下文,全局上下文壓入執行上下文棧。
二、全局執行上下文初始化。
三、執行 checkscope 函數,建立 checkscope函數執行上下文,checkscope 執行上下文被壓入執行上下文棧。
四、checkscope 執行上下文初始化,建立變量對象、做用域鏈、this 等。
五、checkscope 函數執行完畢,checkscope 執行上下文從執行上下文棧彈出。
六、執行 f 函數,建立 f 函數的執行上下文,f 執行上下文被壓入執行上下文棧
七、f 執行上下文初始化,建立變量對象、做用域鏈、this 等
八、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 作到了這一點,從而實現了閉包這個概念。

因此,讓咱們再看一遍實踐角度上閉包的定義:

一、即便建立它的上下文已經銷燬,它仍然存在(好比,內部函數從父函數中返回) 二、在代碼中引用了自由變量

面試必刷題

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

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系列目錄

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

歡迎關注個人我的微信公衆號——指尖的宇宙,更多優質思考乾貨

相關文章
相關標籤/搜索