【譯】理解JavaScript閉包——新手指南

閉包是JavaScript中一個基本的概念,每一個JavaScript開發者都應該知道和理解的。然而,不少新手JavaScript開發者對這個概念仍是很困惑的。javascript

正確理解閉包能夠幫助你寫出更好、更高效、簡潔的代碼。同時,這將會幫助你成爲更好的JavaScript開發者。java

所以,在這篇文章中,我將會嘗試解析閉包內部原理以及它在JavaScript中是如何工做的。數據結構

好,廢話少說,讓咱們開始吧。閉包

什麼是閉包

用一句話來講就是,閉包是一個能夠訪問它外部函數做用域的一個函數,即便這個外部函數已經返回了。這意味着即便在函數執行完以後,閉包也能夠記住及訪問其外部函數的變量和參數。ide

在咱們深刻學習閉包以前,首先,咱們先理解下詞法做用域(lexical scope)。函數

什麼是詞法做用域

JavaScript中的詞法做用域(或者靜態做用域)是指在源代碼物理位置中變量、函數以及對象的可訪問性。舉個例子:學習

let a = 'global';
  function outer() {
    let b = 'outer';
    function inner() {
      let c = 'inner'
      console.log(c);   // prints 'inner'
      console.log(b);   // prints 'outer'
      console.log(a);   // prints 'global'
    }
    console.log(a);     // prints 'global'
    console.log(b);     // prints 'outer'
    inner();
  }
outer();
console.log(a);         // prints 'global'

這裏的inner函數能夠訪問本身做用域下定義的變量和outer函數的做用域以及全局做用域。而outer函數能夠訪問本身做用域下定義的變量已經全局做用域。
因此,上面代碼的一個做用域鏈是這樣的:ui

Global {
  outer {
    inner
  }
}

注意到,inner函數被outer函數的詞法做用域所包圍,而outer函數又被全局做用域所包圍。這就是inner函數能夠訪問outer函數以及全局做用域定義的變量的緣由。線程

閉包的實際例子

在深刻閉包是如何工做以前,咱們先來看下閉包一些實際的例子。code

// 例子1
function person() {
  let name = 'Peter';
  
  return function displayName() {
    console.log(name);
  };
}
let peter = person();
peter(); // prints 'Peter'

在這段代碼中,咱們調用了返回內部函數displayName的person函數,並將該函數存儲在perter變量中。當咱們調用perter函數時(其實是引用displayName函數),名字「Perter」會打印到控制檯。
可是在displayName函數中並無定義任何名爲name到變量,因此即便該函數返回了,該函數也能夠用某種方式訪問其外部函數person的變量。因此displayName函數其實是一個閉包。

// 例子2
function getCounter() {
  let counter = 0;
  return function() {
    return counter++;
  }
}
let count = getCounter();
console.log(count());  // 0
console.log(count());  // 1
console.log(count());  // 2

一樣地,咱們經過調用getCounter函數返回一個匿名內部函數,而且保存到count變量中。因爲count函數如今是一個閉包,能夠在即便在getCounter函數返回後訪問getCounter函數的變量couneter。
可是請注意,counter的值在每次count函數調用時都不會像一般那樣重置爲0。
這是由於,在每次調用count()的時候,都會建立新的函數做用域,可是隻爲getCounter函數建立一個做用域,由於變量counter定義在getCounter函數做用域內,因此每次調用count函數時數值會增長而不是重置爲0。

閉包工做原理

到目前爲止,咱們已經討論了什麼是閉包以及一些實際的例子。下面咱們來了解下閉包在javaScript中的工做原理。
要真正理解閉包在JavaScript中的工做原理,首先,咱們必需要理解JavaScript中的兩個重要的概念:1)執行上下文 2)詞法環境。

執行上下文(Execution Context)

執行上下文是一個抽象的環境,其中的JavaScript代碼會被計算求值和執行。當全局代碼執行時,它在全局執行上下文中執行,函數代碼在函數執行上下文中執行。

當前只能有一個正在運行執行環境(由於JavaScript是單線程語言),它由被稱爲執行堆棧或調用堆棧的堆棧數據結構管理。

執行堆棧是一個具備LIFO(後進先出)結構的堆棧,其中只能在堆棧頂部進行添加或刪除選項。

當前正在運行的執行上下文始終位於堆棧的頂部,當正在執行的函數執行完成後,其執行上下文將從堆棧中彈出移除,而後控制到達堆棧中它下面的執行上下文。

下面咱們看一個代碼片斷更好地理解執行上下文和堆棧。

當以上代碼執行時,JavaScript引擎會建立一個全局執行上下文來執行全局代碼,而後當執行到調用first()函數時,它會爲該函數建立一個新的執行上下文而且將其推送到執行堆棧的頂部。
因此,上面代碼的執行堆棧就以下圖那樣:

當first()函數執行完後,它的執行堆棧就會從堆棧中移除。而後,控制到達下一個執行上下文,就是全局執行上下文了。所以,將會執行全局做用域下剩餘的代碼。

詞法環境(Lexical Envirionment)

每次JavaScript引擎建立一個執行上下文執行函數或者全局代碼時,它還會建立一個新的詞法環境來存儲在該函數執行期間在該函數中定義的變量。

詞法環境是一個包含標識符(identifier)-變量(variable)映射的數據結構。(這裏所說的標識符(identifier)指的是變量或者函數的名稱,而變量(variable)是實際對象[包括函數類型對象]或原始值的引用)。

一個詞法環境有兩個組件:(1)環境數據 (2)對外部環境的引用。

一、環境數據是指變量和函數聲明實際存放的地方。

二、對外部環境的引用意思是說它能夠訪問外部(父級)的詞法環境。這個組件很重要,是理解閉包工做原理的關鍵。

一個詞法環境從概念上看起來像這樣:

lexicalEnvironment = {
  environmentRecord: {
    <identifier> : <value>,
    <identifier> : <value>
  }
  outer: < Reference to the parent lexical environment> // 父級詞法環境引用
}

如今咱們來從新看下以前上面的代碼片斷:

let a = 'Hello World!';
function first() {
  let b = 25;  
  console.log('Inside first function');
}
first();
console.log('Inside global execution context');

當JavaScript引擎建立一個全局執行上下文來執行全局代碼時,它還建立了一個新的詞法環境來存儲在全局做用域定義的變量和函數。所以,全局做用域的詞法環境將以下所示:

globalLexicalEnvironment = {
  environmentRecord: {
      a     : 'Hello World!',
      first : < reference to function object >
  }
  outer: null
}

這裏的外部詞法環境設置爲null,由於全局做用域沒有外部詞法環境。
當引擎爲first()函數建立執行上下文時,它還會建立一個詞法環境來存儲在執行函數期間在該函數中定義的變量。 因此函數的詞彙環境看起來像這樣:

functionLexicalEnvironment = {
  environmentRecord: {
      b    : 25,
  }
  outer: <globalLexicalEnvironment>
}

函數的外部詞法環境設置爲全局詞法環境,由於該函數被源代碼中的全局做用域所包圍。

詳細的閉包示例

如今咱們理解了執行上下文和詞法環境了,下面咱們回到閉包。

例子一

咱們先看下這個代碼塊

function person() {
  let name = 'Peter';
  
  return function displayName() {
    console.log(name);
  };
}
let peter = person();
peter(); // prints 'Peter'

當person函數執行,JavaScript引擎會給這個函數建立一個新的執行上下文和詞法環境。當該函數執行完成後,將返回displayName函數而且分配給到perter變量。
因此它的詞法環境看起來像這樣:

personLexicalEnvironment = {
  environmentRecord: {
    name : 'Peter',
    displayName: < displayName function reference>
  }
  outer: <globalLexicalEnvironment>
}

當person函數執行完成後,它的執行上下文就會從堆棧裏移除。但它的詞法環境仍然在內存裏,是由於它的詞法環境被它內部的displayName函數的詞法環境引用。因此變量在內存中仍然可用。

當peter函數執行(實際上是引用displayName函數),JavaScript引擎會爲該函數建立新的執行上下文和詞法環境。
因此它的詞法環境看起來像這樣:

displayNameLexicalEnvironment = {
  environmentRecord: {
    
  }
  outer: <personLexicalEnvironment>
}

由於displayName函數沒有聲明變量,因此它的環境數據是空的。該函數在執行期間,javaScript引擎將嘗試在該函數的詞法環境中尋找變量name。
由於displayName函數的詞法環境沒有任何變量,因此引擎會到外層的詞法環境尋找,這就是還在內存中的person函數的詞法環境。JavaScript引擎找到了這個變量name而後打印到控制檯。

例子二

function getCounter() {
  let counter = 0;
  return function() {
    return counter++;
  }
}
let count = getCounter();
console.log(count());  // 0
console.log(count());  // 1
console.log(count());  // 2

一樣地,getCounter函數的詞法環境是這樣的:

getCounterLexicalEnvironment = {
  environmentRecord: {
    counter: 0,
    <anonymous function> : < reference to function>
  }
  outer: <globalLexicalEnvironment>
}

這個函數返回一個匿名函數而且把它分配到變量count。
當這個count函數執行,它的詞法環境看起來是這樣的:

countLexicalEnvironment = {
  environmentRecord: {
  
  }
  outer: <getCountLexicalEnvironment>
}

當count函數被調用,Javascript引擎會嘗試在該函數詞法環境查找變量counter。一樣地,由於它的環境數據是空的,因此引擎將到該函數外層詞法環境查找。
所以,在第一次調用count函數以後getCounter函數的詞法環境是這樣的:

getCounterLexicalEnvironment = {
  environmentRecord: {
    counter: 1,
    <anonymous function> : < reference to function>
  }
  outer: <globalLexicalEnvironment>
}

在每次調用count函數,Javascript引擎都會爲count函數建立一個新的詞法環境,遞增count變量而且更新getCounter函數的詞法環境以表示作了變動。

結語

因此咱們學習了什麼是閉包和閉包的原理。閉包是JavaScript的基本概念,每一個JavaScript開發者都應該理解的。熟悉這些概念將有助於你成爲一個更高效、更好的JavaScript開發者。
若是你以爲這文章對你有幫助,請點個贊!
(完)

後記

以上譯文僅用於學習交流,水平有限,不免有錯誤之處,敬請指正。

原文

原文連接

相關文章
相關標籤/搜索