正在翻譯中。
閉包(Closure)是函式能參照獨立(自由)變數的函式(變數在區域使用,不過定義在封閉的作用域中)換言之,這些函式能「記得」被建立時的環境。
語法作用域(Lexical scoping)
思考這個例子:
function init() { var name = "Mozilla"; // name 是個由 init 建立的局部變數 function displayName() { // displayName() 是內部函式,一個閉包 alert(name); // 使用了父函式宣告的變數 } displayName(); } init();
init()
建立了局部變數 name
與隨後稱作 displayName()
的函式。displayName()
是個在 init()
內定義的內部函式,且只在該函式內做動。displayName()
自己並沒有局部變數,不過它可以訪問外面函式的變數、因而能取用在父函式宣告的變數 name
。
跑一下這個程式碼就會發現它能動。這是個語法作用域的例子:在 JavaScript 中,變數的作用域由其原始碼的所在地決定(很明顯地)巢狀函式也能訪問在外層宣告的變數。
閉包
再思考這個例子:
function makeFunc() { var name = "Mozilla"; function displayName() { alert(name); } return displayName; } var myFunc = makeFunc(); myFunc();
若你執行這個程式碼,它會與前例 init()
有相同結果:字串 Mozilla 會以 JavaScript alert 提示顯示出來。但箇中不同、且頗具趣味處,乃內部函式 displayName()
竟在外部函式執行前,從其回傳。
這段程式碼能跑,看來卻不大直覺:通常,函式內的局部變數,只會在函式的執行期間存在。當 makeFunc()
執行完,預期 name 變數再也不能用也頗合理。而這程式碼雖然能跑,卻很明顯地不如預期。
這局的謎底是 myFunc
成了一個閉包。閉包是個特殊物件、裡面包含兩個東西:一個函式,以及一個創立函式時的環境。環境由閉包創立時,作用域內的任何局部變數組成。在此例中,myFunc
乃為建立時合併 displayName
函式與 Mozilla 字串之閉包。
這裡有個更有趣的例子:makeAdder
函式:
function makeAdder(x) { return function(y) { return x + y; }; } var add5 = makeAdder(5); var add10 = makeAdder(10); console.log(add5(2)); // 7 console.log(add10(2)); // 12
在此,我們定義一個帶有 x
參數並回傳新函式的函式 makeAdder(x)
。該新函式又帶有 y
參數並回傳了 x
與 y
的總和。
本質上 makeAdder
乃為函式工廠:它是個建立給定值、並與其參數求和之函式。上例中我們的函式工廠建立了兩個新函式:一個給參數加 5,令一個則是 10。
add5
與 add10
都是閉包。他們共享函式的定義,卻保有不同的環境:在 add5
的 x
是 5。而在 add10
的 x
則是 10。
實用的閉包
這就是閉包的原理了──但它真有什麼用嗎?讓我們想想它們的實際意義吧。閉包讓你把一些資料(環境)與操控他們的函式相關聯。很明顯地,這與把一些資料(物件屬性)與一些方法的相關聯的物件導向程式設計(object oriented programming)相似。
因此,在使用只含一個方法的物件之處,通常也可以使用閉包。
在 Web 中,試圖做這種事的情況還蠻普遍的。我們寫的大多數 web JavaScript 程式碼屬於 event-based 的:我們定義了一些行為,接著把它與用戶觸發事件(例如點擊或按鍵)連結起來。程式碼通常會以 callback 的形式連結:也就是一個處理事件回應的函式。
這裡有個實際的例子:假設我們想在網頁上,加個能調整文字大小的按鈕。其中一個方法是用像素指定 body 元素的 font-size,接著透過相對的 em 單位,設置其他頁面的其他因素(如 headers)個大小:
body { font-family: Helvetica, Arial, sans-serif; font-size: 12px; } h1 { font-size: 1.5em; } h2 { font-size: 1.2em; }
我們的互動式文字大小按鈕,可以改變 body 元素的 font-size 屬性(property)並藉由相對單位令頁面其他元素接受相應調整。
以下是 JavaScript:
function makeSizer(size) { return function() { document.body.style.fontSize = size + 'px'; }; } var size12 = makeSizer(12); var size14 = makeSizer(14); var size16 = makeSizer(16);
size12
、size14
、size16
現在是能調整字體大小到分別為 12、14、與 16 像素的函式。而我們能如下例一般,把他們附加到按鈕上(本例為連結):
document.getElementById('size-12').onclick = size12; document.getElementById('size-14').onclick = size14; document.getElementById('size-16').onclick = size16;
<a href="#" id="size-12">12</a> <a href="#" id="size-14">14</a> <a href="#" id="size-16">16</a>
使用閉包模擬私有方法
Languages such as Java provide the ability to declare methods private, meaning that they can only be called by other methods in the same class.
JavaScript does not provide a native way of doing this, but it is possible to emulate private methods using closures. Private methods aren't just useful for restricting access to code: they also provide a powerful way of managing your global namespace, keeping non-essential methods from cluttering up the public interface to your code.
Here's how to define some public functions that can access private functions and variables, using closures which is also known as the module pattern:
var counter = (function() { var privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } }; })(); console.log(counter.value()); // logs 0 counter.increment(); counter.increment(); console.log(counter.value()); // logs 2 counter.decrement(); console.log(counter.value()); // logs 1
There's a lot going on here. In previous examples each closure has had its own environment; here we create a single environment which is shared by three functions: counter.increment
, counter.decrement
, and counter.value
.
The shared environment is created in the body of an anonymous function, which is executed as soon as it has been defined. The environment contains two private items: a variable called privateCounter
and a function called changeBy
. Neither of these private items can be accessed directly from outside the anonymous function. Instead, they must be accessed by the three public functions that are returned from the anonymous wrapper.
Those three public functions are closures that share the same environment. Thanks to JavaScript's lexical scoping, they each have access to the privateCounter
variable and changeBy
function.
You'll notice we're defining an anonymous function that creates a counter, and then we call it immediately and assign the result to the counter
variable. We could store this function in a separate variable makeCounter
and use it to create several counters.
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(); alert(counter1.value()); /* Alerts 0 */ counter1.increment(); counter1.increment(); alert(counter1.value()); /* Alerts 2 */ counter1.decrement(); alert(counter1.value()); /* Alerts 1 */ alert(counter2.value()); /* Alerts 0 */
Notice how each of the two counters maintains its independence from the other. Its environment during the call of the makeCounter()
function is different each time. The closure variable privateCounter
contains a different instance each time.
Using closures in this way provides a number of benefits that are normally associated with object oriented programming, in particular data hiding and encapsulation.
Creating closures in loops: A common mistake
Prior to the introduction of the let
keyword in ECMAScript 6, a common problem with closures occurred when they were created inside a loop. Consider the following example:
<p id="help">Helpful notes will appear here</p> <p>E-mail: <input type="text" id="email" name="email"></p> <p>Name: <input type="text" id="name" name="name"></p> <p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) { document.getElementById('help').innerHTML = help; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } } } setupHelp();
The helpText
array defines three helpful hints, each associated with the ID of an input field in the document. The loop cycles through these definitions, hooking up an onfocus
event to each one that shows the associated help method.
If you try this code out, you'll see that it doesn't work as expected. No matter what field you focus on, the message about your age will be displayed.
The reason for this is that the functions assigned to onfocus
are closures; they consist of the function definition and the captured environment from the setupHelp
function's scope. Three closures have been created, but each one shares the same single environment. By the time the onfocus
callbacks are executed, the loop has run its course and the item variable (shared by all three closures) has been left pointing to the last entry in the helpText
list.
One solution in this case is to use more closures: in particular, to use a function factory as described earlier on:
function showHelp(help) { document.getElementById('help').innerHTML = help; } function makeHelpCallback(help) { return function() { showHelp(help); }; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = makeHelpCallback(item.help); } } setupHelp();
This works as expected. Rather than the callbacks all sharing a single environment, the makeHelpCallback
function creates a new environment for each one in which help
refers to the corresponding string from the helpText
array.
One other solution to this problem is:
function showHelp(help) { document.getElementById('help').innerHTML = help; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { (function() {var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); }})(); //immediate execution and listener attachment } } setupHelp();
If you don't want to use more closures, you can use the let
keyword of ES6 :
function showHelp(help) { document.getElementById('help').innerHTML = help; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { let item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } } } setupHelp();
In the above case, we used let instead of var, so every closure will bind the every block scoped variable, so it will work perfectly without using any extra closure.
Performance considerations
It is unwise to unnecessarily create functions within other functions if closures are not needed for a particular task, as it will negatively affect script performance both in terms of processing speed and memory consumption.
For instance, when creating a new object/class, methods should normally be associated to the object's prototype rather than defined into the object constructor. The reason is that whenever the constructor is called, the methods would get reassigned (that is, for every object creation).
Consider the following impractical but demonstrative case:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); this.getName = function() { return this.name; }; this.getMessage = function() { return this.message; }; }
The previous code does not take advantage of the benefits of closures and thus could instead be formulated:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } MyObject.prototype = { getName: function() { return this.name; }, getMessage: function() { return this.message; } };
However, redefining the prototype is not recommended, so the following example is even better because it appends to the existing prototype:
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; };
The above code can also be written in a cleaner way with the same result:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } (function() { this.getName = function() { return this.name; }; this.getMessage = function() { return this.message; }; }).call(MyObject.prototype);
In the two previous examples, the inherited prototype can be shared by all objects and the method definitions need not occur at every object creation. See Details of the Object Model for more details.