JavaScript:今後再也不怕閉包

閉包就好像從JavaScript中分離出來的一個充滿神祕色彩的未開化世界,只有最勇敢的人才能到達那裏。——《你不知道的JavaScript 上卷》javascript

一、起源

js閉包很長一段時間裏都是讓我頭疼的一個東西。工做中遇到相似這樣的代碼就很怕:html

需求

頁面內三個按鈕,點擊按鈕控制檯輸出按鈕在全部按鈕中的序號,序號從1開始java

說明

固然,實際的應用中咱們通常不會有這麼單純的需求,也不會寫這麼刻意的代碼,這裏咱們爲了學習,強行挖個坑,本身再填坑。chrome

上代碼

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>閉包</title>
</head>
<body>
    點擊顯示按鈕序號<br>
    <button>click</button>
    <button>click</button>
    <button>click</button>
    <script> var btns = document.querySelectorAll("button"); for(var i=0;i<btns.length;i++){ btns[i].onclick = function(){ console.log(i+1); } } </script>
</body>
</html>
複製代碼

不懂閉包前,我就以爲,這很優雅啊,按鈕在集合btns中的索引+1正好就是知足需求的。興奮地趕忙自測,咔咔咔連點三下。數組

結果

點擊按鈕.png

why.jpg

當時心裏表情大概就像上面這個哥們。但仍是在工位上故做鎮定地趕忙百度了下。bash

修正

百度和修改後的for循環就變成了這個樣子,用一個閉包存一下數組索引i的值傳給閉包內的函數。微信

for(var i=0;i<btns.length;i++){
    btns[i].onclick = (function(tmpI){
       return function(){
            console.log(tmpI+1);
       } 
    })(i);
}
複製代碼

click,click,click。大功告成! 閉包

點擊按鈕修正.png

二、原理

工做中知足了當時的需求也就立馬沉迷代碼擼下一個功能去了。但對閉包的詳細原理知之甚少,相關問題稍微發生點變化,就又可能讓本身雲裏霧裏。近來專門找了幾本書,刻意攻克了下,纔算開始瞭解了閉包。函數

如上問題的解析

代碼再貼一下:學習

var btns = document.querySelectorAll("button");
for(var i=0;i<btns.length;i++){
    btns[i].onclick = function(){
        console.log(i+1);
    }
}
複製代碼

上面代碼不能按需實現功能的問題在於: 一、for循環內只是將匿名函數引用賦給onclick方法,在函數被調用時纔會去實時取i的值 二、i爲全局變量,在onclick方法觸發時,i的值已被i++的操做變成了4

for(var i=0;i<btns.length;i++){
    btns[i].onclick = (function(tmpI){
       return function(){
            console.log(tmpI+1);
       } 
    })(i);
}
複製代碼

上述代碼,onclick方法得到的匿名函數經過當即執行函數返回,當即執行函數內又經過傳參的方式,將i的值傳入,用tmpI變量保存起來。確保了外部的i++操做不會影響函數內的tmpI的值的變化,就解決了問題。

閉包定義

"閉包(closure)是一個函數在建立時容許自身函數訪問並操做該自身函數以外的變量時所建立的做用域。"——《JavaScript 忍者祕籍》

這是我以爲相對來講比較易懂的對閉包的解釋。關鍵要素有兩個: 一、函數建立時產生。 二、容許自身訪問並操做函數以外的變量。 上述代碼return的匿名函數中訪問了其函數以外的tmpI變量,造成了閉包。

按書中定義,以下咱們你們天天都在寫的代碼也屬於閉包。

var outerValue = "外部變量";

function outerFunction(){
    console.log("outerValue",outerValue);
}
outerFunction();
複製代碼

按定義,outerFunction確實訪問了其外的outerValue值,屬於閉包。但由於函數定義和外部變量都處在全局做用域中,該做用域從未消失過,因此咱們也沒以爲這有什麼特殊的地方。經過實際的chrome調試發現,chrome也不會標記此類的閉包。如下,咱們和chrome保持一致,再也不特別說明全局做用域閉包。

再舉個栗子

var outerValue ="外部變量";
var later;

function outerFunction(){
	var innerValue = "內部值";

	function innerFunction(paramValue){
		console.log(outerValue,"Inner can see the outerValue.");
		console.log(innerValue,"Inner can see the innerValue.");
		console.log(paramValue,"Inner can see the paramValue.");
		console.log(tooLate,"Inner can see the tooLate.");
	}
	later = innerFunction;
}

console.log(tooLate,"Outer can't see the tooLate.");
var tooLate = "outerFunction以後聲明的變量";

outerFunction();
later("later參數");
複製代碼

輸出結果以下:

ninja.png

上述例子引用自《JavaScript 忍者祕籍》,筆者作了部分修改。這裏innerFunction函數建立時造成了閉包,其訪問了outerFunction中的innerValue。 其他部分的代碼書中是爲了說明閉包的三種更有趣的性質。
一、內部函數的參數是包含在閉包中的。(這是顯而易見的)
二、做用域以外的全部變量,即使是函數聲明以後的那些聲明,也都包含在閉包中。(在調用later時能夠訪問到tooLate變量)
三、相同的做用域內,還沒有聲明的變量不能進行提早引用。(如代碼中先打印的tooLate爲undefined,我以爲這也是顯而易見的。)

chrome中對outerFunction閉包的標識以下:

outerFunction閉包.png

在調用outerFunction,定義innerFunction時,訪問了innerValue,造成了outerFunction閉包。

三、延伸

按個人理解,js有閉包的概念是由於js設計之初沒有塊級做用域,只能經過函數來限制變量的有效範圍。當有了塊級做用域,其實也就再也不須要寫閉包。如開頭例子若是用ES6 let改寫,也可實現需求功能。

var btns = document.querySelectorAll("button");
for(let i=0;i<btns.length;i++){
    btns[i].onclick = function(){
        console.log(i+1);
    }
}
複製代碼

讀者能夠試下,這也是知足需求的。這是由於使用let定義i,ES6作了處理,每次循環的i都有各自的做用域,其值不會互相影響,函數調用時,其仍然保持了原值。

文中內容有錯誤,或讀者對此仍有疑惑的,歡迎你們在評論中留言或者加微信一塊兒探討學習。

主要參考資料

《你不知道的JavaScript 上卷》——[美]Kyle Simpson 《JavaScript 忍者祕籍》——[美]John Resig,Bear Bibeault

相關文章
相關標籤/搜索