閉包詳解一

lce_shou發表於2018-05-25

在正式學習閉包之前,請各位同學一定要確保自己對詞法作用域已經非常的熟悉了,如果對詞法作用域還不夠熟悉的話,可以先看:

前言

現在去面試前端開發的崗位,如果你對面試官也是個前端,並且不是太水的話,你有很大的概率會被問到JavaScript中的閉包。因為這個閉包這個知識點真的很重要,還非常難掌握。

什麼是閉包

什麼是閉包,你可能會搜出很多答案....

《JavaScript高階程式設計》這樣描述:

閉包是指有權訪問另一個函式作用域中的變數的函式;

《JavaScript權威指南》這樣描述:

從技術的角度講,所有的JavaScript函式都是閉包:它們都是物件,它們都關聯到作用域鏈。

《你不知道的JavaScript》這樣描述:

當函式可以記住並訪問所在的詞法作用域時,就產生了閉包,即使函式是在當前詞法作用域之外執行。

我最認同的是《你不知道的JavaScript》中的描述,雖然前面的兩種說法都沒有錯,但閉包應該是基於詞法作用域書寫程式碼時產生的自然結果,是一種現象!你也不用為了利用閉包而特意的建立,因為閉包的在你的程式碼中隨處可見,只是你還不知道當時你寫的那一段程式碼其實就產生了閉包。

講解閉包

上面已經說到,當函式可以記住並訪問所在的詞法作用域時,就產生了閉包,即使函式是在當前詞法作用域之外執行

看一段程式碼

function fn1() {
	var name = 'iceman';
	function fn2() {
		console.log(name);
	}
	fn2();
}
fn1();
複製程式碼

如果是根據《JavaScript高階程式設計》和《JavaScript權威指南》來說,上面的程式碼已經產生閉包了。fn2訪問到了fn1的變數,滿足了條件“有權訪問另一個函式作用域中的變數的函式”,fn2本身是個函式,所以滿足了條件“所有的JavaScript函式都是閉包”。

這的確是閉包,但是這種方式定義的閉包不太好觀察。

再看一段程式碼:

function fn1() {
	var name = 'iceman';
	function fn2() {
		console.log(name);
	}
	return fn2;
}
var fn3 = fn1();
fn3();
複製程式碼

這樣就清晰地展示了閉包:

  • fn2的詞法作用域能訪問fn1的作用域

  • 將fn2當做一個值返回

  • fn1執行後,將fn2的引用賦值給fn3

  • 執行fn3,輸出了變數name

我們知道通過引用的關係,fn3就是fn2函式本身。執行fn3能正常輸出name,這不就是fn2能記住並訪問它所在的詞法作用域,而且fn2函式的執行還是在當前詞法作用域之外了。

正常來說,當fn1函式執行完畢之後,其作用域是會被銷燬的,然後垃圾回收器會釋放那段記憶體空間。而閉包卻很神奇的將fn1的作用域存活了下來,fn2依然持有該作用域的引用,這個引用就是閉包

總結:某個函式在定義時的詞法作用域之外的地方被呼叫,閉包可以使該函式極限訪問定義時的詞法作用域

注意:對函式值的傳遞可以通過其他的方式,並不一定值有返回該函式這一條路,比如可以用回撥函式:

function fn1() {
	var name = 'iceman';
	function fn2() {
		console.log(name);
	}
	fn3(fn2);
}
function fn3(fn) {
	fn();
}
fn1();
複製程式碼

本例中,將內部函式fn2傳遞給fn3,當它在fn3中被執行時,它是可以訪問到name變數的。

所以無論通過哪種方式將內部的函式傳遞到所在的詞法作用域以外,它都回持有對原始作用域的引用,無論在何處執行這個函式都會使用閉包。

再次解釋閉包

以上的例子會讓人覺得有點學院派了,但是閉包絕不僅僅是一個無用的概念,你寫過的程式碼當中肯定有閉包的身影,比如類似如下的程式碼:

function waitSomeTime(msg, time) {
	setTimeout(function () {
		console.log(msg)
	}, time);
}
waitSomeTime('hello', 1000);
複製程式碼

定時器中有一個匿名函式,該匿名函式就有涵蓋waitSomeTime函式作用域的閉包,因此當1秒之後,該匿名函式能輸出msg。

另一個很經典的例子就是for迴圈中使用定時器延遲列印的問題:

for (var i = 1; i <= 10; i++) {
	setTimeout(function () {
		console.log(i);
	}, 1000);
}
複製程式碼

在這段程式碼中,我們對其的預期是輸出1~10,但卻輸出10次11。這是因為setTimeout中的匿名函式執行的時候,for迴圈都已經結束了,for迴圈結束的條件是i大於10,所以當然是輸出10次11咯。

究其原因:i是宣告在全域性作用中的,定時器中的匿名函式也是執行在全域性作用域中,那當然是每次都輸出11了。

原因知道了,解決起來就簡單了,我們可以讓i在每次迭代的時候,都產生一個私有的作用域,在這個私有的作用域中儲存當前i的值。

for (var i = 1; i <= 10; i++) {
	(function () {
		var j = i;
		setTimeout(function () {
			console.log(j);
		}, 1000);
	})();
}
複製程式碼

這樣就達到我們的預期了呀,讓我們用一種比較優雅的寫法改造一些,將每次迭代的i作為實參傳遞給自執行函式,自執行函式中用變數去接收:

for (var i = 1; i <= 10; i++) {
	(function (j) {
		setTimeout(function () {
			console.log(j);
		}, 1000);
	})(i);
}
複製程式碼

閉包的應用

閉包的應用比較典型是定義模組,我們將操作函式暴露給外部,而細節隱藏在模組內部:

function module() {
	var arr = [];
	function add(val) {
		if (typeof val == 'number') {
			arr.push(val);
		}
	}
	function get(index) {
		if (index < arr.length) {
			return arr[index]
		} else {
			return null;
		}
	}
	return {
		add: add,
		get: get
	}
}
var mod1 = module();
mod1.add(1);
mod1.add(2);
mod1.add('xxx');
console.log(mod1.get(2));
複製程式碼

關於閉包還有很多要講,這裡先講解比較基礎的概念,接下來還會有更精彩的內容。

特別注意

可以關注我的公眾號:icemanFE,接下來持續更新技術文章!

公眾號.png

相關文章