譯者:閉包都被討論爛了,不理解閉包都很差意思說本身會js,但我看到這篇文章仍是感受眼前一亮,也讓我對閉包有了一些新的理解,而且涉及了一些類和原型鏈的知識,這是一篇2012年的文章,稍微有點早,內容也略微基礎,可是很明晰,但願能給讀者帶來新的理解。
閉包(Closure) 是javascript這門語言中有些複雜而且充滿誤解的特性。簡言之,閉包是一個對象,這個對象包含一個方法(function)和該方法建立時環境的引用(reference to the enviroment)。爲了徹底理解閉包,咱們還須要理解兩個js中的特性,一個是一級方法(first-class function),另外一個是內部方法(inner function)。javascript
在js中,方法是頭等公民,由於它能夠被輕易轉換成其餘數據類型。好比,一級方法能夠實時構建而且賦值給一個變量。也能夠傳遞給其餘方法,或者經過其餘方法返回。除了知足這些標準之外,方法也擁有本身的屬性和方法。
經過下述例子,咱們來看一下一級方法的能力。html
var foo = function() { alert("Hello World!"); }; var bar = function(arg) { return arg; }; bar(foo)();
譯者注:省略原文對代碼的文字解釋,這裏體現的是一級方法能夠返回參數,參數能夠是另一個一級函數,返回的結果還能夠調用。
內部方法或者說嵌套方法,是指定義在其餘方法內部的方法,每當外部方法被喚起,內部方法的實例就被建立。下面的例子反應內部方法的使用,add方法是外部方法,doAdd是內部方法。java
function add(value1, value2) { function doAdd(operand1, operand2) { return operand1 + operand2; } return doAdd(value1, value2); } var foo = add(1, 2); // foo equals 3
這個例子中,一個重要的特性是,內部方法獲取到了外部方法的做用域,這意味着內部方法可以使用外部方法的變量,參數等。例子中add()的參數value1,value2傳遞給doAdd()的operand1,operand2參數。然而這並無必要,由於doAdd能夠直接獲取value1,value2。因此上面的例子咱們還能夠這麼寫:閉包
function add(value1, value2) { function doAdd() { return value1 + value2; } return doAdd(); } var foo = add(1, 2); // foo equals 3
內部方法獲取外部方法的做用域,便造成了一個閉包。典型的場景是外部函數將其內部方法返回,內部方法保持了外部環境的引用,並保存了做用域下的全部變量。
一下例子展現閉包如何建立並使用。dom
function add(value1) { return function doAdd(value2) { return value1 + value2; }; } var increment = add(1); var foo = increment(2); // foo equals 3
說明:函數
function increment(value2) { return 1 + value2; }
閉包能夠實現不少功能。好比將回調函數綁定指定參數。咱們說兩個讓你的生活和開發變得更簡單的場景。this
閉包結合setTimeout和setInterval很是有用,閉包容許你向回調函數傳入指定參數,好比下面的例子,每秒鐘在給指定dom插入字符串。spa
<!DOCTYPE html> <html lang="en"> <head> <title>Closures</title> <meta charset="UTF-8" /> <script> window.addEventListener("load", function() { window.setInterval(showMessage, 1000, "some message<br />"); }); function showMessage(message) { document.getElementById("message").innerHTML += message; } </script> </head> <body> <span id="message"></span> </body> </html>
遺憾的是,IE不支持向setInterval的回調傳參,IE中頁面不會展示「some message」而是「undefined」(無值傳入showMessage()),解決這個問題,能夠經過閉包將指望值綁定於回調函數裏,咱們能夠改寫如上代碼:prototype
window.addEventListener("load", function() { var showMessage = getClosure("some message<br />"); window.setInterval(showMessage, 1000); }); function getClosure(message) { function showMessage() { document.getElementById("message").innerHTML += message; } return showMessage; }
2.模擬私有屬性
絕大多數面向對象的程序語言支持對象的私有屬性,然而js不是純正的面向對象的語言,所以也沒有私有屬性的概念。不過,咱們能夠經過閉包來模擬私有屬性。回想一下,閉包包含了一份其建立環境的引用,這份引用已經不在當前做用域中了,所以這份引用只能在閉包中訪問,這本質上就是私有屬性。
看以下例子(譯者:省略對代碼的文字描述):code
function Person(name) { this._name = name; this.getName = function() { return this._name; }; }
這裏有一個嚴重的問題,由於js不支持私有屬性,因此咱們無法阻止別人修改實例的name字段,好比咱們建立一個Person實例叫Colin,而後能夠將他的名字改爲Tom。
var person = new Person("Colin"); person._name = "Tom"; // person.getName() now returns "Tom"
沒有人願意不經贊成就被別人更名字,爲了阻止這種狀況的發生,經過閉包讓_name字段變成私有。看以下代碼,注意這裏的_name是Person構造器的本地變量,而不是對象的屬性,閉包造成了,由於外層方法Person對外暴露了一個內部方法getName。
function Person(name) { var _name = name;// 注:區別在這裏 this.getName = function() { return _name; }; }
如今,當getName被調用,可以保證返回的是最初傳入類構造器的值。咱們依然能夠爲對象添加新的_name屬性,但這並不影響閉包getName最初綁定的值,下面的代碼證實,_name字段,事實私有。
var person = new Person("Colin"); person._name = "Tom"; // person._name is "Tom" but person.getName() returns "Colin"
正確理解閉包如何工做什麼時候使用很是重要,而理解何時不該該用它也一樣重要。過分使用閉包會致使腳本執行變慢並消耗額外內存。因爲閉包太容易建立了,因此很容易發生你都不知道怎麼回事,就已經建立了閉包的狀況。本節咱們說幾種場景要注意避免閉包的產生。
1.循環中
循環中建立出閉包會致使結果異常。下例中,頁面上有三個按鈕,分別點擊彈出不一樣的話術。然而實際運行,全部的按鈕都彈出button4的話術,這是由於,當按鈕被點擊時,循環已經執行完畢,而循環中的變量i也已經變成了最終值4.
<!DOCTYPE html> <html lang="en"> <head> <title>Closures</title> <meta charset="UTF-8" /> <script> window.addEventListener("load", function() { for (var i = 1; i < 4; i++) { var button = document.getElementById("button" + i); button.addEventListener("click", function() { alert("Clicked button " + i); }); } }); </script> </head> <body> <input type="button" id="button1" value="One" /> <input type="button" id="button2" value="Two" /> <input type="button" id="button3" value="Three" /> </body> </html>
去解決這個問題,必須在循環中去掉閉包(譯者:這裏的閉包指的是click事件回調函數綁定了外層引用i),咱們能夠經過調用一個引用新環境的函數來解決。下面的代碼中,循環中的變量傳遞給getHandler函數,getHandler返回一個閉包(譯者:這個閉包指的是getHandler返回的內部方法綁定傳入的i參數),獨立於原來的for循環。
function getHandler(i) { return function handler() { alert("Clicked button " + i); }; } window.addEventListener("load", function() { for (var i = 1; i < 4; i++) { var button = document.getElementById("button" + i); button.addEventListener("click", getHandler(i)); } });
2.構造函數裏的非必要使用
類的構造函數裏,也是常常會產生閉包的錯誤使用。咱們已經知道如何經過閉包設置類的私有屬性,而若是當一個方法不須要調用私有屬性,則形成的閉包是浪費的。下面的例子中,Person類增長了sayHello方法,可是它沒有使用私有屬性。
function Person(name) { var _name = name; this.getName = function() { return _name; }; this.sayHello = function() { alert("Hello!"); }; }
每當Person被實例化,建立sayHello都要消耗時間,想象一下有大量的Person被實例化。更好的實踐是將sayHello放入Person的原型鏈裏(prototype),原型鏈裏的方法,會被全部的實例化對象共享,所以節省了爲每一個實例化對象去建立一個閉包(譯者:指sayHello),因此咱們有必要作以下修改:
function Person(name) { var _name = name; this.getName = function() { return _name; }; } Person.prototype.sayHello = function() { alert("Hello!"); };