深刻理解閉包

閉包

  • 概念
閉包是一種特殊的對象。

它由兩部分組成:執行上下文(代號A),以及在該執行上下文中建立的函數(代號B)。閉包

當執行B時,若是訪問了A中的變量對象中的值,那麼閉包就會產生。函數

有時候以函數B的名字代指這裏生成的閉包。而在Chrome中,則以執行上下文A的函數名代指閉包。工具

只須要知道一個閉包對象,由A、B共同組成便可。性能

// demo1
function foo() {
    var a = 100;
    var b = 200;
    
    function bar() {
        return a + b;
    }
    
    return bar;
}

var bar = foo();
bar();

上面例子中,首先執行上下文foo,在foo中定義了函數bar,然後經過對外返回bar的方式讓bar得以執行。當bar執行時,訪問了foo內部的變量a和b。所以這個時候閉包產生。優化

在Chrome中經過斷點調試的方式能夠逐步分析該過程,此時閉包產生,用foo代指,以下圖:this

圖片描述

上圖中,箭頭所指的正是閉包。其中Call Stack爲當前的函數執行棧,Scope爲當前正在被執行函數的做用域,Local爲當前活動對象。spa

來看一個很是有意思的例子:設計

// demo2
function add(x) {
    return function _add(y) {
        return x + y;
    }
}

add(2)(3); // 5

上面的例子有閉包產生嗎?
固然有。當內部函數_add被調用執行時,訪問了add函數變量對象中的x,這個時候,閉包就會產生,以下圖,必定要記住,函數參數的變量傳遞給函數以後也會加到變量對象中。調試

圖片描述

下面代碼會產生閉包嗎?code

// demo3
var name = "window";

var person = {
    name: "perter",
    getName: function() {
        return function() {
            return this.name;
        };
    }
};

var getName = person.getName();
var _name = getName();
console.log(_name);

getName在執行時,它的this其實指向的是window對象,而這個時候並無造成閉包的環境,所以這個例子沒有閉包

若是按照下面的方式進行改動呢?

// demo4
// 改動一
var name = "window";

var person = {
    name: "perter",
    getName: function() {
        return function() {
            return this.name;
        };
    }
};

var getName = person.getName();
// 利用call的方式讓this指向person對象
var _name = getName.call(person);
console.log(_name);

// demo5
// 改動二
var name = "window";

var person = {
    name: "perter",
    getName: function() {
        // 利用變量保存的方式保證其訪問的是person對象
        var self = this;
        return function() {
            return self.name;
        };
    }
};

var getName = person.getName();
var _name = getName();
console.log(_name);

分別利用call與變量保存的方式保證this指向的都爲person對象。因此demo4(因爲Chrome已作優化,因此在Chrome調試工具中沒有顯示閉包)和demo5都產生了閉包

  • 閉包與垃圾回收機制
瞭解垃圾回收機制原理都知道當一個值失去引用以後就會被標記,而後被垃圾回收機制回收並釋放空間。

當一個函數的執行上下文運行完畢以後,內部的全部內容都會失去引用而被垃圾回收機制回收。

閉包的本質就是在函數的外部保持了內部變量的引用,所以閉包會阻止垃圾回收機制進行回收

下面用一個例子來證實這一點:

// demo6
function foo1() {
    var n = 99;
    
    nAdd = function() {
        n += 1;
    };
    
    return foo2() {
        console.log(n);
    };
}

var result = foo1();
result(); // 99

nAdd();

result(); // 100

從上面的例子能夠看出,由於nAdd都訪問了foo1中的n,所以它們都與foo1造成了閉包。這個時候變量n的引用被保留了下來。由於foo2(result)與nAdd執行時都訪問了n,aAdd每運行一次就會將n加1,因此上例的執行結果很是符合咱們的認知。

認識到 閉包中保存的內容不會被釋放以後,咱們在使用 閉包時就要保持足夠的警戒性。若是濫用 閉包,極可能會由於內存的緣由致使程序性能過差。
  • 閉包與做用域鏈

結合下面的例子思考一下,閉包會致使函數的做用域鏈發生改變嗎?

// demo7
var fn = null;

function foo() {
    var a = 2;
    
    function innerFoo() {
        console.log(a);
    }
    
    fn = innerFoo; // 將innerFoo的引用賦值給全局變量中的fn
}

function bar() {
    var a = 3;
    fn(); // 此處保留innerFoo的引用
}

foo();
bar(); // 2

在上面的例子中,foo內部的innerFoo訪問了foo的變量a。所以當innerFoo執行時會有閉包產生。全局變量fn在foo內部獲取了innerFoo的引用,並在bar中執行。

innerFoo斷點調試圖以下:

圖片描述

在這裏須要特別注意的地方是函數調用棧(Call Stack)與做用域鏈(Scope)的區別。

由於函數調用棧實際上是在代碼執行時才肯定的,而做用域規則在代碼編譯階段就已經肯定,雖然做用域鏈是在代碼執行時才生成的,可是它的規則並不會在執行時發生改變。

因此,閉包的存在並不會致使做用域鏈發生變化。


參考資料:
JavaScript高級程序設計

JavaScript核心技術開發揭祕

相關文章
相關標籤/搜索