JavaScript 自由變量與閉包

在講閉包之前,我們先了解一下什麼是自由變量。

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

示例代碼:

let a = 1;

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

foo();

上述代碼中,對於函數 foo 來說,a 既不是函數參數也不是函數的局部變量的變量,因此 a 屬於自由變量。

什麼是閉包

在 ECMAScript 中,閉包(Closure)是指能夠訪問自由變量的函數。

按照以上的概念,我們可以說所有函數都是閉包,因爲它們都是在創建的時候就保存了上層上下文的作用域鏈,觀察如下代碼:

let a = 1;

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

foo(); // > 1

(function () {

  let a = 2;
  foo(); // > 1

})();

ECMAScript 使用的是詞法作用域(Lexical scoping,又稱靜態作用域),即在函數創建時,就保存上層上下文的作用域鏈,上述代碼中,foo 函數創建時,其所使用的變量 a 是已經在上下文中靜態保存好的,因此在執行 foo() 時 a 的值爲 1。

而任何函數,在其創建時保存的上層上下文的作用域中都有全局的自由變量 global(在瀏覽器中,global 爲 window),因此說所有函數都是閉包。

實踐中的閉包

上面說的是理論上的閉包,但在實踐中,閉包不僅僅只是能夠訪問自由變量的函數,閉包是指引用了自由變量的,並且被引用的自由變量將和這個函數一同存在的函數,在創建該函數的上下文已經銷燬時,該函數仍然存在。

示例代碼:

function foo(){
	let a = 1;

	return function(){
		console.log(a)
	}
}
foo()

上述代碼中,foo 函數執行後返回了一個匿名函數,該函數引用了自由變量 a,而在 foo() 執行完畢後,創建該函數的環境已經銷燬,但該函數並沒有被銷燬,因此 foo() 的返回值就是一個閉包。

閉包會使引用的自由變量不能被清除,這就使得閉包比其他函數佔用內存更多,但這也是閉包的強大之處,以下是一個使用閉包的例子:

let foo = function() {

  let a = 1;

  return {
  	add:function(){
  		return ++a;
  	},
  	sub:function(){
  		return --a;
  	}
  }
}

let f = foo();
f.add(); // 2
f.sub(); // 1

再來看一個面試中經常遇到的題目:

let data = [];

for (let i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0](); // 3
data[1](); // 3
data[2](); // 3

這三個函數創建時均使用的是已經在上下文中靜態保存好的變量 i,而在 for 循環結束時,變量 i 的值爲 3,當 data[0]() 執行時,其所引用的自由變量 i 的值爲 3,因此輸出 3。

我們的目標是輸出 0、1、2,上面的例子顯然無法實現這個需求,利用閉包可以很輕鬆地解決這個問題:

let data = [];

for (let i = 0; i < 3; i++) {
  data[i] = (function(x) {
    return function () {
      alert(i);
    };
  })(i); // 傳入"i"}

data[0](); // 0
data[1](); // 1
data[2](); // 2
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/eCAHSKtdUkmOaq-0zJbWQg