理解 Javascript 的閉包

jordans7p9j1 8年前發布 | 15K 次閱讀 閉包 JavaScript開發 JavaScript

來自: http://zilongshanren.com/blog/2016-02-28-understand-javascript-closure.html

因為最近幾個月一直在做 Cocos Creator 這個項目,大部分時間都在與 Javascript 打交道,所以接下來我有必要寫幾篇文章介紹一下 JS 里面幾個比較讓人迷惑的地方:閉包,變量作用域,變量提升和 this 綁定。

今天這篇文章我們來聊一聊閉包。

什么是閉包?

閉包是一個函數,它在函數內部創建,并且攜帶了自身創建時的所處環境信息(比如變量信息和其它函數信息)。

上面這段話是引用至 MDN,它很清楚地說明了什么是閉包。

閉包 = 函數內部創建的函數(或者簡稱內部函數) + 該函數創建時所處環境信息

所以閉包并不等于匿名函數,雖然也有人稱這些在函數內部創建的函數為閉包函數,但是我覺得其實并不準確。

我們看一下下面這段代碼:

function init() {
    var name = "Zilongshanren"; // name 是在 init 函數里面創建的變量
    // displayName() 是一個內部函數,即一個閉包。注意,它不是匿名的。
    function displayName() {
        console.log(name);
    }
    //當 displayName 函數返回后,這個函數還能訪問 init 函數里面定義的變量。
    return displayName;
}
var closure = init();
closure();
Zilongshanren
undefined

displayName 是一個在 init 函數內部創建的函數,它攜帶了 init 函數內部作用域的所有信息,比如這里的 name 變量。當 displayName 函數返回的時候,它本身攜帶了當時創建時的環境信息,即 init 函數里面的 name 變量。

</div>

閉包有什么作用?

在理解什么是閉包之后,接下來你可能會問:這東西這么難理解,它到底有什么用啊?

因為在 Js 里面是沒有辦法創建私有方法的,它不像 java 或者 C++有什么 private 關鍵字可以定義私有的屬性和方法。 Js 里面只有函數可以創建出屬于自身的作用域的對象,Js 并沒有塊作用域!這個我后面會再寫一篇文章詳細介紹。

編程老鳥都知道,程序寫得好,封裝和抽象要運用得好!不能定義私有的屬性和方法,意味著封裝和抽象根本沒法用。。。

不能定義私有的東西,所有變量和函數都 public 顯然有問題, Global is Evil!

閉包是我們的救星!

我們看一下下面這段代碼:

var makeCounter = function() {
    var privateCounter = 0;
    function changeBy(val) {
        privateCounter += val;
    }
    return {
        increment: function() {
            changeBy(1);
        },
        decrement: function() {
            changeBy(-1);
        },
        value: function() {
            return privateCounter;
        }
    }
};

var counter1 = makeCounter();
var counter2 = makeCounter();
console.log(counter1.value()); /* Alerts 0 */
counter1.increment();
counter1.increment();
console.log(counter1.value()); /* Alerts 2 */
counter1.decrement();
console.log(counter1.value()); /* Alerts 1 */
console.log(counter2.value()); /* Alerts 0 */
0
2
1
0
undefined

這里面的 privateCounter 變量和 changeBy 都是私有的,對于 makeCounter 函數外部是完全不可見的。這樣我們通過 makeCounter 生成的對象就把自己的私有數據和私有方法全部隱藏起來了。

這里有沒有讓你想到點什么?

哈哈,這不就是 OO 么?封裝數據和操作數據的方法,然后通過公共的接口調用來完成數據處理。

當然,你也許會說,我用原型繼承也可以實現 OO 呀。沒錯,現在大部分人也正是這么干的,包括我們自己。不過繼承這個東西,在理解起來總是非常困難的,因為要理解一段代碼,你必須要理解它的所有繼承鏈。如果一旦代碼出 bug 了,這將是非常難調試的。

扯遠了,接下來,讓我們看看如何正確地使用閉包。

</div>

如何正確地使用閉包?

閉包會占用內存,也會影響 js 引擎的執行效率,所以,如果一段代碼被頻繁執行,那么要謹慎考慮在這段代碼里面使用閉包。

讓我們來看一個創建對象的函數:

function MyObject(name, message) {
    this.name = name.toString();
    this.message = message.toString();
    this.getName = function() {
        return this.name;
    };

    this.getMessage = function() {
        return this.message;
    };
}

var myobj = new MyObject();

var myobj = new MyObject(); 每一次被調用生成一個新對象的時候,都會生成兩個閉包。如果你的程序里面有成千上萬個這樣的 MyObject 對象,那么會額外多出很多內存占用。

正確的做法應該是使用原型鏈:

function MyObject(name, message) {
    this.name = name.toString();
    this.message = message.toString();
}
MyObject.prototype.getName = function() {
    return this.name;
};
MyObject.prototype.getMessage = function() {
    return this.message;
};

var myobj = new MyObject();

現在 MyObject 原型上面定義了兩個方法,當我們通過 new 去創建對象的時候,這兩個方法只會在原型上面存有一份。

</div>

閉包的性能如何?

閉包也是一個函數,但是它存儲了額外的環境信息,所以理論上它比純函數占用更多的內存,而且 Js 引擎在解釋執行閉包的時候消耗也更大。不過它們之間的性能差別在 3%和 5%之間(這是 Google 上得到的數據,可能不是太準確)。

但是,閉包的好處肯定是大大的。多使用閉包和無狀態編程,讓 Bug 從此遠離我們。

</div>

小結

面向對象是窮人的閉包(OO is an poor man's closure.)

理解了閉包,你就能理解大部分 FP 范式的 Js 類庫及其隱藏在背后的設計思想。當然僅有閉包還不夠,你還需要被 FP 和無狀態,lambda calculus 等概念洗腦。

</div>

 本文由用戶 jordans7p9j1 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!