理解 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>