js的嵌套函數與閉包函數


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、閉包示例:

比如:
init() 創建了一個局部變量 name 和一個名為 displayName() 的函數。displayName() 是定義在 init() 里的內部函數,並且僅在 init() 函數體內可用。請注意,displayName() 沒有自己的局部變量。然而,因為它可以訪問到外部函數的變量,所以 displayName() 可以使用父函數 init() 中聲明的變量 name 。
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;
};

在前面的兩個示例中,繼承的原型可以為所有對象共享,不必在每一次創建對象時定義方法。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM