看了這一篇你還不理解什麼是閉包——我直播喫翔!

看了這一篇你還不理解什麼是閉包——我直播喫翔!

一. 前言

許多剛剛接觸JavaScript的小夥伴可能對閉包一臉懵逼,難於理解。對於那些有一點JavaScript使用經驗但未真正理解閉包的人來說,理解閉包會使你功力大增。恰巧這篇文章正是爲你準備的。
有些小夥伴對閉包有了一定理解,但是並不清楚在JavaScript具體有哪些應用,現在我告訴你。在JavaScript中閉包無處不在,只是你還沒有發現它。閉包是基於詞法作用域書寫代碼時產生的自然結果,你不需要爲了利用它而有意識的創建閉包,因爲閉包已經在你的代碼中無處不在。
相信我,看完這篇文章,你一定會恍然大悟,原來我的代碼中已經到處都是閉包了。

二.準備知識

爲了理解閉包,你必須對詞法作用域作用域鏈有一定的理解。

三.閉包

閒話少說,直接上乾貨
對於閉包直接了當的定義:

當函數可以記住並訪問所在的詞法作用域時,就產生了閉包 (你不知道的JavaScript)
閉包是指有權訪問另一個函數作用域中的變量的函數(JavaScript高級程序設計)

直接上代碼

function foo() {
	var a = 2;
	return function fun1() {
		console.log(a)
	}
}
var fun2 = foo()
fun2()  // 2

在上面的例子中,fun1能夠訪問foo的內部作用域,我們把fun1作爲一個值返回。在foo()執行後,把foo()的返回值 fun1 賦值給fun2並調用fun2。打印出了結果2.
此時,我們可以說fun1記住並訪問了所在的詞法作用域 或者說 fun2訪問了另一個函數作用域中的變量(fun2在全局作用域中聲明,訪問了foo的內部作用域)
由於引擎有自動的垃圾回收機制,在foo()執行後(不再使用),通常foo的整個內部作用域會被銷燬,對內存進行回收。閉包的神奇之處正是可以阻止這件事情的發生,因爲fun1依然持有對該作用域的引用,這個引用就叫做閉包。
無論使用何種方式對函數類型的值進行傳遞,當函數在別處調用時,都可以看到閉包。
直接傳遞

function foo() {
	var a = 2;
	function baz() {
		console.log(a);
	}
	bar(baz); //對函數類型進行值得傳遞
}
function bar(fn) {
	fn(); // 閉包產生了
}

或者間接的傳遞

var fn;
function foo(){
	var a = 2;
	function baz() {
		console.log(a);
	}
	fn = baz; //對函數類型進行值得傳遞
}
function bar(fn) {
	fn(); //閉包產生了
}
foo()
bar()

無論通過什麼手段將內部函數傳遞到所在的詞法作用域之外,它都會保持對定義時作用域的引用,這個函數無論在何處執行,都產生了閉包。

現在你理解閉包了嗎?思考一下你的代碼中產生了哪些閉包?

四. 無處不在的閉包

1.定時器

function wait(message) {
	setTimeout(function timer() {
		console.log(message)
	}, 1000)
}
wait('hello 閉包')

將函數timer傳遞給setTimeout(), 內部函數timer 保持了對wait的作用域的引用,1000毫秒後打印出‘hello 閉包’。

2. 或者你使用過jQuery

function doSomeing(selector,doWhat) {
	$(selector).onClick(function(){
		console.log(doWhat)
	})
}
doSomeing('#dom1','dowhat')
doSomeing('#dom2','dowhat2')

jquery的click事件處理函數保持了對doSomeing的作用域的引用。當click事件發生時,閉包也產生了。

3.ajax

function foo(someData){
	$.ajax({
		data:data,
		url:url,
		success:function(result){
			console.log(someData)
		}
	})
}
foo('我也是閉包')

ajax的success回調函數保持了對foo的作用域的引用,這也是閉包。

現在你理解了嗎?在定時器,事件監聽器,Ajax請求,跨窗口通信,Web Workers 或者其他任何的異步(或同步)任務中,只要使用了回調函數,實際上都是在應用閉包。

還有一個IIFE模式(立即執行函數)需要單獨說一下

var a = 2;
(function(){
	console.log(a) //2
})()

雖然它能夠正確的執行,但是嚴格意義上來說,它並不是一個閉包,因爲它不是在本身的詞法作用域之外執行的。我認爲,因爲它是在原始定義時的詞法作用域中立即執行,也是保存了對原始定義時的詞法作用域的引用。它確確實實產生了閉包的效果。

五. 循環和閉包

思考下面的代碼

for (var i=1;i<=5;i++){
	setTimeout(function timer() {
		console.log(i)
	},i*1000)
}

正常情況下,我們對這段代碼的預期是每隔一秒輸出一個數字,1-5。但是實際上並不會這樣,這段代碼會每間隔一秒輸出一個數字6。這是爲什麼?

首先6從哪裏來,當timer執行時,for循環早已結束,終止條件是i=6。
有些小夥伴可能會不明白了,timer不是創建了閉包,保持了對i的引用嗎?
沒錯。for循環了5次,創建了5個閉包,但是都是保持了對同一個i的引用(i是全局變量)。所以當timer執行時,i = 6,輸出6,沒毛病。
那麼如何達到我們想要的效果,我們需要更多的閉包。循環過程中每個迭代都需要一個閉包

for (var i=1;i<=5;i++){
	(function(j){
		setTimeout(function timer() {
			console.log(j)
		},j*1000)
	})(i)
}

立即執行函數創建了一個新的作用域,使得延遲函數的的回調可以將新的作用域封閉在每個迭代內部。問題解決。

現在你理解閉包了嗎? 大聲的告訴我。你的代碼中還有其他閉包的應用嗎?現在停下你手中的事情,查看你最近的寫的代碼,看看有沒有閉包的應用吧☺

如果這篇文章有幫到你,點個贊再走唄!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章