js的嵌套函數與閉包函數
先看一下代碼示例:
function f(){ var cnt=0; return function(){ return ++cnt;} } var fa=f();//將函數f的的返回值給變量fa // fa(); //對fa的函數調用 console.log(fa());//1 console.log(fa());//2 console.log(fa());//3
函數的返回值是函數(對象的引用),這里將其賦值給變量fn。在調用fn時,其輸出結果每次都會自增加1
從表面看,閉包(closure)具有狀態的函數,或者也可以將閉包的特征理解為:其相關的局部變量在函數調用結束后會繼續存在
一、閉包的原理
1.1 嵌套的函數聲明:
閉包的前提條件是需要在函數聲明的內部聲明另一個函數(即嵌套的函數聲明)貼一下函數函數聲明的simple example:
function f(){ function g(){ console.log('g is called'); } g(); } f()// g is called
在函數f的聲明中包含函數g的聲明以及調用語句。再調用函數f時,就間接地調用了函數g。為了更好理解該過程,在此對其內部機制進行說明。
在javaScript中,調用函數時將會隱式地生成call對象。為了方便起見,我們將調用函數f生成的call對象稱作call-f對象。在函數調用完成之后,call對象將被銷毀。
函數f內的函數g的聲明將會生成一個與函數的g相對應function對象。其名稱g是call-f對象的屬性。由 於每一次調用函數都會獨立生成call對象,因此在調用函數g時將會隱式地生成另一個call對象。為了方便起見,我們將該call對象稱作call-g對象。
離開函數g之后,call-g對象將被自動銷毀。類似的,離開函數f之后,call-f對象也就自動銷毀。此時,由於屬性g將與call-g對象一起被銷毀,所以由g所引用的function對象將會失去其引用,而最終(通過垃圾回收機制)被銷毀。
1.2嵌套函數與作用域
對上面代碼稍稍修改:
function f(){ var n=123; function g(){ console.log("n is"+n); console.log('g is called'); } g(); } f(); 運行結果: js>f(); n is 123 g is called'
在內層進行聲明函數g可以訪問外層的函數f的局部變量(在這里指變量n),對於嵌套聲明的函數,內部的函數將會首先查找被調用時所生成的call對象的屬性,之后之后在查找外層函數的call對象的屬性。這一機制被稱為作用鏈。
1.3嵌套函數的返回
上面的代碼稍稍修改
function f(){ var n=123; function g(){ console.log("n is"+n); console.log('g is called'); } return g; } js> f(); function g(){ console.log("n is"+n); console.log('g is called'); }
由於return語句,函數將會返回一個function對象(的引用)。調用函數f的結果是一個function對象。這時,雖然會生成與函數f相對應的call對象(call-f對象)(並在離開函數f后被銷毀),但由於不會調用函數g,所以此時還不會生成與之相對應的call對象(call-g對象),請對此多加注意。
二、閉包
2.1、作用域
待更新
2.2、閉包示例:
function init() { var name = "Mozilla"; // name 是一個被 init 創建的局部變量 function displayName() { // displayName() 是內部函數,一個閉包 console.log(name); // 使用了父函數中聲明的變量 } displayName(); } init();
運行這段代碼的效果和之前 init() 函數的示例完全一樣。其中不同的地方(也是有意思的地方)在於內部函數 displayName() 在執行前,從外部函數返回。。在本例子中,myFunc 是執行 makeFunc 時創建的 displayName 函數實例的引用。displayName 的實例維持了一個對它的詞法環境(變量 name 存在於其中)的引用。因此,當 myFunc 被調用時,變量 name 仍然可用,其值 Mozilla 就被傳遞到alert中
function makeFunc() { var name = "Mozilla"; function displayName() { console.log(22, name); } return displayName; } var myFunc = makeFunc(); myFunc(); //22 "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
在這個示例中,我們定義了 makeAdder(x) 函數,它接受一個參數 x ,並返回一個新的函數。返回的函數接受一個參數 y,並返回x+y的值。
從本質上講,makeAdder 是一個函數工廠 — 他創建了將指定的值和它的參數相加求和的函數。在上面的示例中,我們使用函數工廠創建了兩個新函數 — 一個將其參數和 5 求和,另一個和 10 求和。
add5 和 add10 都是閉包。它們共享相同的函數定義,但是保存了不同的詞法環境。在 add5 的環境中,x 為 5。而在 add10 中,x 則為 10。
三、閉包的實際應用
css:
<style> body { font-family: Helvetica, Arial, sans-serif; font-size: 12px; } h1 { font-size: 1.5em; } h2 { font-size: 1.2em; } </style>
html:
<hr> <a href="#" id="size-12">12</a> <a href="#" id="size-14">14</a> <a href="#" id="size-16">16</a>
js:
window.onload = function(){ function makeSizer(size) { return function() { document.body.style.fontSize = size + 'px'; }; } var size12 = makeSizer(12); var size14 = makeSizer(14); var size16 = makeSizer(16); document.getElementById('size-12').onclick = size12; document.getElementById('size-14').onclick = size14; document.getElementById('size-16').onclick = size16; }
用閉包模擬私有方法:
//***** 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 */
在之前的示例中,每個閉包都有它自己的詞法環境;而這次我們只創建了一個詞法環境,為三個函數所共享:Counter.increment,Counter.decrement 和 Counter.value。
該共享環境創建於一個立即執行的匿名函數體內。這個環境中包含兩個私有項:名為 privateCounter 的變量和名為 changeBy 的函數。這兩項都無法在這個匿名函數外部直接訪問。必須通過匿名函數返回的三個公共函數訪問。
這三個公共函數是共享同一個環境的閉包。多虧 JavaScript 的詞法作用域,它們都可以訪問 privateCounter 變量和 changeBy 函數。
四、在循環中創建閉包常見誤區
htm代碼片段:
<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>
js代碼片段:
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();
運行這段代碼后,您會發現它沒有達到想要的效果。無論焦點在哪個input上,顯示的都是關於年齡的信息。
原因是賦值給 onfocus 的是閉包。這些閉包是由他們的函數定義和在 setupHelp 作用域中捕獲的環境所組成的。這三個閉包在循環中被創建,但他們共享了同一個詞法作用域,在這個作用域中存在一個變量item。這是因為變量item使用var進行聲明,由於變量提升,所以具有函數作用域。當onfocus的回調執行時,item.help的值被決定。由於循環在事件觸發之前早已執行完畢,變量對象item(被三個閉包所共享)已經指向了helpText的最后一項。
改進方法一:
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();
改進方法二:
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); } })(); // 馬上把當前循環項的item與事件回調相關聯起來 } } setupHelp();
避免使用過多的閉包,可以用let關鍵詞:
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();
例如,在創建新的對象或者類時,方法通常應該關聯於對象的原型,而不是定義到對象的構造器中。原因是這將導致每次構造器被調用時,方法都會被重新賦值一次(也就是,每個對象的創建)。
考慮以下示例:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); this.getName = function() { return this.name; }; this.getMessage = function() { return this.message; }; }
在上面的代碼中,我們並沒有利用到閉包的好處,因此可以避免使用閉包。修改成如下:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } MyObject.prototype = { getName: function() { return this.name; }, getMessage: function() { return this.message; } };
但我們不建議重新定義原型。可改成如下例子:
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; };
在前面的兩個示例中,繼承的原型可以為所有對象共享,不必在每一次創建對象時定義方法。