閉包詳解一

在正式學習閉包以前,請各位同窗必定要確保本身對詞法做用域已經很是的熟悉了,若是對詞法做用域還不夠熟悉的話,能夠先看:前端

前言

如今去面試前端開發的崗位,若是你對面試官也是個前端,而且不是太水的話,你有很大的機率會被問到JavaScript中的閉包。由於這個閉包這個知識點真的很重要,還很是難掌握。面試

什麼是閉包

什麼是閉包,你可能會搜出不少答案....bash

《JavaScript高級程序設計》這樣描述:閉包

閉包是指有權訪問另外一個函數做用域中的變量的函數;函數

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

從技術的角度講,全部的JavaScript函數都是閉包:它們都是對象,它們都關聯到做用域鏈。學習

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

當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包,即便函數是在當前詞法做用域以外執行。spa

我最認同的是《你不知道的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
相關文章
相關標籤/搜索