深刻理解JavaScript閉包之什麼是閉包

前言

在看本篇文章以前,能夠先看一下以前的文章 深刻理解JavaScript 執行上下文深刻理解JavaScript做用域,理解執行上下文和做用域對理解閉包有很大的幫助。javascript

須要回憶的一些知識點:html

  1. 做用域和詞法做用域,做用域就是查找變量(去哪兒找,怎麼找)的一套規則。詞法做用域在你寫代碼的時候就肯定了。JavaScript是基於詞法做用域的語言,經過變量定義的位置就能知道變量的做用域。
  2. 做用域鏈:當某個函數第一次被調用時,會建立一個執行環境及相應的做用域鏈,並把做用域鏈賦值給一個特殊的內部屬性 [[Scope]] 。而後,使用 thisarguments 和其餘命名參數的值來初始化函數的活動對象。但在做用域中,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象處於第三位,...直至做用做用域鏈終點的全局執行環境。

一個真實的面試場景

  • A: 什麼是閉包
  • B: 函數 foo 內部聲明瞭一個變量 a, 在函數外部是訪問不到的,閉包就是可使得在函數外部訪問函數內部的變量
  • A:額,不太準確,那你說一下閉包有什麼用途吧
  • B: ...,很差意思,一會兒想不起來了
  • A:今天面試就到這兒了,有後續再通知你。

閉包差很少是面試必問的一個知識點了,記得幾年前剛出來找實習的時候問的是這個,如今出去面試仍是一直在問這個。頗有必要好好學習一下,不只僅是由於面試,更是由於它在代碼中也很是常見。前端

什麼是閉包

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

function foo() {
    var a = 1; // a 是一個被 foo 建立的局部變量
    function bar() { // bar 是一個內部函數,是一個閉包
        console.log(a); // 使用了父函數中聲明的變量
    }
    return bar();
}
foo(); // 1

foo() 函數中聲明瞭一個內部變量 a , 在函數外部是沒法訪問的,bar() 函數是 foo() 函數內部的函數,此時 foo 內部的全部局部變量,對 bar 都是可見的,反過來就不行,bar 內部的局部變量,對 foo 就是不可見的。這就是javaScript特有的」做用域鏈「。面試

function foo() {
    var a = 1; // a 是一個被 foo 建立的局部變量
    function bar() { // bar 是一個內部函數,是一個閉包
        console.log(a); // 使用了父函數中聲明的變量
    }
    return bar;
}
const myFoo = foo();
myFoo();

這段代碼和上面的代碼運行結果徹底一致,其中不一樣的地方就是在於內部函數 bar 在執行前,從外部函數返回。foo() 執行後,將其返回值(也就是內部的 bar 函數)賦值給變量 myFoo 並調用 myFoo(), 實際上只是經過不一樣的標識符引用調用了內部的函數 bar()閉包

foo() 函數執行後,正常狀況下 foo() 的整個內部做用域被銷燬,佔用的內存被回收。可是如今的 foo的內部做用域 bar() 還在使用,因此不會對其進行回收。bar() 依然持有對改做用域的引用,這個引用就叫作閉包。這個函數在定義的詞法做用域之外的地方被調用。閉包使得函數能夠繼續訪問定義時的詞法做用域。app

用一句話描述:閉包是指有權訪問另外一個函數做用域中變量的函數。建立閉包最多見的方式就是,在一個函數內部建立另外一個函數。異步

常見的一些閉包

function foo(a) {
    setTimeout(function timer(){
        console.log(a)
    }, 1000)
}
foo(2);

foo執行1000ms 後,它的內部做用域不會消失,timer函數依然保有 foo 做用域的引用。timer函數就是一個閉包。函數

定時器,事件監聽器,Ajax請求,跨窗口通訊,Web Workers或者其餘異步或同步任務中,只要使用回調函數,實際上就是閉包。post

循環和閉包

for(var i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i);
    }, i * 1000);
}

上面的這段代碼,預期是每隔一秒,分別輸出 0, 1, 2, 3, 4, 但實際上依次輸出的是 5, 5, 5, 5, 5。首先解釋5是從哪裏來的,這個循環的終止條件是 i 再也不 < 5,條件首次成立時 i 的值是5,所以,輸出顯示的是循環結束時 i 的最終值。

延遲函數的回調會在循環結束時才執行。事實上,當定時器運行時即便每一個迭代中執行的都是 setTimeout(.., 0),全部的回調函數依然是在循環結束後纔會被執行。所以每次輸出一個 5來。

咱們的預期是每一個迭代在運行時都會給本身 "捕獲" 一個 i 的副本。可是實際上,根據做用域的原理,儘管循環中的五個函數都是在各自迭代中分別定義的,可是他們都封閉在一個共享的全局做用域中,所以實際上只有一個 i。即全部函數共享一個 i 的引用。

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

代碼改爲上面這樣,就能夠按照咱們指望的方式進行工做了。這樣修改以後,在每次迭代內使用 IIFE(當即執行函數)會爲每一個迭代都生成一個新的做用域,使得延遲函數的回調能夠將新的做用域封閉在每一個迭代內部,每一個迭代內部都會含有一個具備正確值的變量能夠訪問。

for(let i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i);
    }, i * 1000);
}

使用 ES6 塊級做用域的 let 替換 var 也能夠達到咱們的目的。

爲何老是 JavaScript 中閉包的應用都有着關鍵詞 「return」, javaScript 祕密花園 中有一段話解釋到:閉包是JavaScript 一個很是重要的特性,這意味着當前做用域老是可以訪問外部做用域的變量。 由於函數是 JavaScript 中惟一擁有自身做用域的結構,所以閉包的建立依賴於函數。

須要注意的點

容易致使內存泄漏
閉包會攜帶包含它的函數做用域,所以會比其餘函數佔用更多的內存。過分使用閉包會致使內存佔用過多,因此要謹慎使用閉包。

關於this的狀況

在閉包中使用 this 對象。

this對象是運行時基於函數的執行環境綁定的。全局函數中,this指向 window,當函數被做用某個對象的方法調用時,this指向這個對象,不過匿名函數的執行環境具備全局性,所以其this對象一般指向window。以前這篇 一文理解this、call、apply、bind文章中也專門講了this。
var name = 'The window';

var object = {
    name: 'my Object',
    getName: function() {
        return function() {
            return this.name;
        }
    }
}
console.log(object.getName()()); // The window 非嚴格模式下
  1. 上面代碼建立了一個全局變量 name, 又建立了一個包含 name 屬性的對象,這個對象還包含了一個方法 getName(),它返回一個匿名函數,而匿名函數又返回 this.name
  2. 因爲getName返回一個函數,所以調用 object.getName()() 會當即調用它返回的函數。結果就是返回字符串 「The window 」,即全局 name 變量的值。

爲何匿名函數沒有取得包含做用域的this對象呢?每一個函數在被調用時會自動獲取兩個特殊的變量: this, arguments。內部函數在搜索這兩個變量時,只會搜索到其活動對象爲止,所以永遠不可能直接訪問外部函數的這兩個變量。

不過把外部做用域中的 this對象保存在一個閉包可以訪問到的變量裏,就可讓閉包訪問該對象了。

var name = 'The window';

var object = {
    name: 'my Object',
    getName: function() {
        var that = this; // 把this對象賦值給了 that變量
        return function() {
            return that.name;
        }
    }
}

console.log(object.getName()()); // my Object

上面代碼中把this對象賦值給了 that變量,that變量時包含在函數中的,即時函數返回以後,that也仍然引用這 object,因此調用 object.getName()() 返回 「my Object」

arguments 和 this存在相同的問題,若是想訪問做用域中的 arguments 對象,必須將對該對象的引用保存到另外一個閉包可以訪問的變量中。

有幾種特殊狀況下,this的值可能會意外地發生改變。好比下面的代碼是修改其前面例子的結果。

var name = 'The window';

var object = {
    name: 'my Object',
    getName: function() {
        return this.name
    }
}

console.log(object.getName()); // my Object
console.log((object.getName)()); // my Object
console.log((object.getName = object.getName)()); // The window 非嚴格模式下
  1. 第一個就是正常的調用,打印 「my Object」
  2. 第二個就是在調用這個方法前先給它加上了括號,可是和 object.getName 是同樣的,因此打印爲 "my Object"
  3. 第三個是先執行了一個賦值語句,而後再調用賦值後的結果。由於這個賦值表達式是函數自己,因此此時調用,this 指向的是 window,打印的是 "The window"

關於什麼是閉包就大概說到這裏,下一篇文章會講一下閉包的應用場景。

總結

  • 閉包是指有權訪問另外一個函數做用域中變量的函數。
  • 閉包一般用來建立內部變量,使得 這些變量不能被外部隨意修改,同時又能夠經過指定的接口來操做。

參考

相關文章
相關標籤/搜索