理解 JavaScript 中的閉包

前言

繼上一篇《理解 JavaScript 中的做用域》後,我又馬上寫下了這篇文章,由於這二者是存在關聯的,在理解閉包前,你須要知道做用域。javascript

而對於那些有一點 JavaScript 使用經驗的人來講,理解閉包能夠看作是某種意義上的重生,但這並不簡單,你須要付出很是多的努力和犧牲才能理解這個概念。java

若是你理解了閉包,你會發現即使是沒理解閉包以前,你也用到了閉包,但咱們要作的就是根據本身的意願正確地識別、使用閉包。面試

什麼是閉包

閉包的定義,你須要掌握它才能理解和識別閉包:閉包

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

下面用一些代碼來解釋這個定義:函數

function foo(){
    var a = 2;
    function bar(){
        console.log(a); // 2
    }
    bar();
}
foo();
複製代碼

很明顯這是一個嵌套做用域,而bar的做用域也確實可以訪問外部做用域,但這就是閉包嗎?ui

不,不徹底是,但它是閉包中很重要的一部分:根據詞法做用域的查找規則,它可以訪問外部做用域。spa

下面再來看這段代碼,它清晰地使用了閉包:設計

function foo(){
    var a = 2;
    function bar(){
        console.log(a);
    }
    return bar;
}
var baz = foo();
baz(); // 2 —— 這就是閉包
複製代碼

因爲bar的詞法做用域可以訪問foo的內部做用域,而後咱們把bar這個函數自己看成返回值,而後在調用foo時把bar引用的函數賦值給baz(實際上是兩個標識符引用同一個函數),因此baz可以訪問foo的內部做用域。code

而這裏正是印證前面的定義:函數是在當前詞法做用域以外執行。

其實按正常狀況下,引擎有垃圾回收器用來釋放再也不使用的內存空間,當foo執行完畢時,天然會將其回收,但閉包的神奇之處正是能夠阻止這件事情的發生,由於內部做用域依然存在,bar在使用它。

因爲bar聲明位置的緣由,它涵蓋了foo內部做用域的閉包,使得該做用域可以一直存活,以供bar在以後任什麼時候間進行引用。

bar依然有對該做用域的引用,而這個引用就叫作閉包。

所以,當baz在調用時,它天然可以訪問到foo的內部做用域。

固然,不管使用何種方式對函數類型的值進行傳遞,當函數在別處被調用時均可以觀察到閉包的存在:

function foo(){
    var a = 2;
    function baz(){
        console.log(a);
    }
    bar(baz);
}
function bar(fn){
    fn(); // 2 —— 這也是閉包
}
複製代碼

把內部函數baz做爲fn參數傳遞給bar,當調用fn時,它可以訪問到foo的內部做用域。

傳遞函數也能夠是間接的:

var fn;
function foo(){
    var a = 2;
    function baz(){
        console.log(a);
    }
    fn = baz;
}
foo();
fn(); // 2 —— 這也是閉包
複製代碼

因此:

不管經過何種方式將內部函數傳遞到所在的詞法做用於以外,它都會持有對原始定義做用域的引用,不管在何處執行這個函數都會使用閉包。

閉包的使用

既然前面說閉包無處不在,那不妨看看幾個平時常常看到的片斷,看看閉包的妙用。

function wait(message){
    setTimeout(function timer(){
        console.log(message);
    },1000);
}
wait("Hello, closure!");
複製代碼

將一個內部函數(這裏叫作timer)做爲參數傳遞給setTimeout,而timer可以訪問wait的內部做用域。

若是你使用過jQuery,不難發現下面代碼中也使用了閉包:

function setupBot(name,selector){
    $(selector).click(function activator(){
        console.log("Activating:" + name);
    })
}
setupBot("Closure Bot 1","#btn_1");
setupBot("Closure Bot 2","#btn_2");
複製代碼

本質上不管什麼時候何地,若是將函數( 訪問它們各自的詞法做用域)看成第一級的值類型並處處傳遞, 你就會看到閉包在這些函數中的應用。 在定時器、 事件監聽器、Ajax請求、 跨窗口通訊、Web Workers或者任何其餘的異步( 或者同步)任務中, 只要使用了回調函數,實際上就是在使用閉包!

再來看一個很經典的閉包面試題:

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

正常狀況下,咱們對這段代碼行爲的預期是每秒一次輸出1~5。

但實際上,這段代碼在運行時會以每秒一次的頻率輸出五次6。

爲何?

首先解釋6是從哪裏來的,這個循環的終止條件是i再也不<=5,因此當條件成立時,i等於6。所以,輸出顯示的是循環結束時i的最終值。

也就是咱們陷入了一個這樣的誤區:覺得循環中每一個迭代在運行時都會複製一個i的副本,但根據做用域的工做原理,它們都共享同一個全局做用域,所以實際上只有一個i

要使這段代碼的運行與咱們預期一致,解決方法以下:

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

在這段代碼中咱們使用了IIFE,將i做爲參數j傳遞進去,在每一個迭代IIFE會生成一個本身的做用域,它們接受參數j不同,因此這段代碼可以符合咱們預期地運行。

還有別的解決方案嗎?

是的,使用 ES6 新出的let能夠解決這個問題:

for (let i=1; i<=5; i++){
	setTimeout(function(){
		console.log(i);
    },i*1000);
}
複製代碼

咱們僅僅把var替換爲let就輕鬆地解決了該問題,緣由以下:

  • for中有本身的塊做用域(()是父級做用域,{}是子級做用域)。
  • 使用let可以建立塊做用域的變量。

好了,到如今你應該可以很容易地識別閉包,那麼接下來,咱們繼續介紹閉包更高級的用法。

假設咱們有這樣一個對象:

var box = {
    age : 18,
}
console.log(box.age); // 18
複製代碼

然而這裏有一個問題,那就是屬性age能夠隨意改變,若是咱們使用閉包,就能夠實現私有化,將age屬性保護起來,只作容許的修改。

var box = (function (){
    var age = 18;
    return {
        birthday : function(){
            age++;
        },
        sayAge : function(){
            console.log(age);
        }
    }
})();
box.birthday();
box.sayAge(); // 19
複製代碼

這樣咱們就保證age屬性只能增長,而不能減小,畢竟沒有人可以越活越年輕。

注意:

  1. 其實對象也有方法能夠控制屬性的修改,但這裏主要講述閉包,就不過多贅述。
  2. 使用閉包可以輕鬆實現本來在 JavaScript 較複雜的設計。

後記

其實當你理解了閉包以後,你就會發現一切都是那麼的理所固然,就彷彿它本該如此。

最後,若是你已經理解了閉包而且想練習一下,那麼我能夠出一道題目給你:

實現一個add函數,功能:add(1)(2)(3); // 6

難一點的:

實現一個add函數,功能:add(3)(‘*’)(3); // 9

有幾點:

  1. add函數能夠被無限調用。
  2. 調用完畢後將結果輸出到控制檯。

感謝觀看!

注:此文爲原創文章,如需轉載,請註明出處。

相關文章
相關標籤/搜索