這篇文章帶給大家的內容是關於帶你進一步理解js閉包(詳細),有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。
譯者:閉包都被討論爛了,不理解閉包都不好意思說自己會js,但我看到這篇文章還是感覺眼前一亮,也讓我對閉包有了一些新的理解,並且涉及了一些類和原型鏈的知識,這是一篇2012年的文章,稍微有點早,內容也略微基礎,但是很明晰,希望能給讀者帶來新的理解。
閉包(Closure) 是javascript這門語言中有些複雜且充滿誤解的特性。簡言之,閉包是一個對象,這個物件包含一個方法(function)和該方法創建時環境的引用(reference to the enviroment)。為了完全理解閉包,我們還需要理解兩個js中的特性,一個是一級方法(first-class function),另一個是內部方法(inner function)。
一級方法/First-Class Functions
在js中,方法是頭等公民,因為它可以輕易轉換成其他資料型別。例如,一級方法可以即時建構並且賦值給一個變數。也可以傳遞給其他方法,或透過其他方法返回。除了滿足這些標準以外,方法也擁有自己的屬性和方法。
透過下述例子,我們來看一級方法的能力。
var foo = function() { alert("Hello World!"); }; var bar = function(arg) { return arg; }; bar(foo)();
內部方法/Inner Functions
內部方法或說嵌套方法,是指定義在其他方法內部的方法,每當外部方法被喚起,內部方法的實例就被創建。下面的範例反應內部方法的使用,add方法是外部方法,doAdd是內部方法。
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
建立閉包/Creating Closures
內部方法取得外部方法的作用域,便形成了一個閉包。典型的場景是外部函數將其內部方法傳回,內部方法保持了外部環境的引用,並保存了作用域下的所有變數。
一下範例展示閉包如何建立並使用。
function add(value1) { return function doAdd(value2) { return value1 + value2; }; } var increment = add(1); var foo = increment(2); // foo equals 3
說明:
add傳回了內部方法doAdd,doAdd呼叫了add的參數,閉包建立。
value1是add方法的本機變量,對doAdd來說是非本機變數(非本機變數指變數既不在函數體本身,也不在全域),value2是doAdd的本機變數。
當add(1)被調用,一個閉包被創建並儲存在increment中,在該閉包的引用環境中,value1綁定了1,被綁定的1相當於「封鎖」在這個函數中,這也是「閉包」這個名字的由來。
當increment(2)被調用,進入閉包函數,這意味著攜帶value1為1的doAdd被調用,因此該閉包本質上可以當做如下函數:
function increment(value2) { return 1 + value2; }
何時使用閉包?
閉包可以實現很多功能。例如將回調函數綁定指定參數。我們說兩個讓你的生活和開發變得更簡單的場景。
配合定時器
閉包結合setTimeout和setInterval非常有用,閉包可讓你向回呼函數傳入指定參數,例如下面的例子,每秒鐘在給指定dom插入字串。
<!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()),解決這個問題,可以透過閉包將期望值綁定於回呼函數裡,我們可以改寫如上程式碼:
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不是純正的物件導向的語言,因此也沒有私有屬性的概念。不過,我們可以透過閉包來模擬私有屬性。回想一下,閉包包含了一份其創建環境的引用,這份引用已經不在當前作用域中了,因此這份引用只能在閉包中訪問,這本質上就是私有屬性。
看如下例子(譯者:省略對程式碼的文字描述):
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!"); };
需要记得一些事情
闭包包含了一个方法,以及创建它的代码环境引用
闭包会在外部函数包含内部函数的情况下形成
闭包可以轻松的帮助回调函数传入参数
类的私有属性可以通过闭包模拟
类的构造器中使用闭包不是一个好主意,将它们放到原型链中
以上是帶你進一步理解js閉包(詳細)的詳細內容。更多資訊請關注PHP中文網其他相關文章!