JavaScript閉包使用姿式指南

JavaScript閉包使用姿式指南

引言

閉包就是指可以訪問另外一個函數做用域的變量的函數,閉包就是一個函數,可以訪問其餘函數的做用域中的變量,js有一個全局對象,在瀏覽器下是window,node下是global,全部的函數都在這個對象下,也能訪問這個對象下的變量,這也就是說,js中的全部函數都是閉包html

閉包的定義

函數與對其狀態即詞法環境(lexical environment)的引用共同構成閉包(closure)。也就是說,閉包可讓你從內部函數訪問外部函數做用域。在JavaScript,函數在每次建立時生成閉包。[[1]]

MDN對閉包的定義中說道了詞法環境和引用同時也說道了每次建立時生成閉包
參考代碼node

const eg = ()=>{
    let a ='測試變量' // 被eg建立的局部變量
    let inner = ()=>{ // eg的內部函數,一個閉包
        console.log(a) // 使用了父函數中聲明的變量
    }
    return inner // inner就是一個閉包函數 能夠訪問到eg函數的做用域
}

來個有趣的例子吧

function init() {
    var name = "Mozilla"; // name 是一個被 init 建立的局部變量
    function displayName() { // displayName() 是內部函數,一個閉包
        alert(name); // 使用了父函數中聲明的變量
    }
   displayName();
 }
 init();

因爲js做用域的緣由,dispplayName能夠訪問到父級做用域init的變量name,這點母庸質疑編程

那麼再看這個例子windows

function makeFunc() {
    var name = "Mozilla";
    function displayName() {
        alert(name);
    }
    return displayName;
  }
var myFunc = makeFunc();
myFunc();

這段代碼和以前的代碼執行結果徹底同樣,其中的不一樣 — 也是有意思的地方 — 在於內部函數 displayName() 在執行前,被外部函數返回。你極可能認爲它沒法執行,那麼咱們再改變一下代碼瀏覽器

var name2 = 123
function makeFunc() {
    var name = "Mozilla";
    function displayName() {
        alert(name2);
    }
    return displayName;
  }
var myFunc = makeFunc();
myFunc();

你幾乎不用想就能知道結果確定是123那麼咱們在返回以前的代碼,爲何你就沒法確定代碼的執行結果了呢閉包

答案是,JavaScript中的函數會造成閉包。 閉包是由函數以及建立該函數的詞法環境組合而成。請仔細閱讀這段話,js的閉包是由函數及建立該函數的詞法環境組合而成,建立它的詞法環境有這個變量,全部直接使用這個變量,沒有則向上查找,直至在全局環境都找不到,返回undefindapp

那麼咱們再把例子換一下編程語言

var object = {
     name: ''object",
     getName: function() {
        return function() {
             console.info(this.name)
        }
    }
}
object.getName()()    // underfined

這個時候this指向哪裏呢?答案是全局由於裏面的閉包函數是在window做用域下執行的,也就是說,this指向windows函數

如今咱們換個例子吧

function outer() {
     var  a = '變量1'
     var  inner = function () {
            console.info(a)
     }
     return inner    // inner 就是一個閉包函數,由於他可以訪問到outer函數的做用域
}
var  inner = outer()   // 得到inner閉包函數
inner()   //"變量1"

當程序執行完var inner = outer(),其實outer的執行環境並無被銷燬,由於他裏面的變量a仍然被被inner的函數做用域鏈所引用,當程序執行完inner(), 這時候,inner和outer的執行環境纔會被銷燬調;《JavaScript高級編程》書中建議:因爲閉包會攜帶包含它的函數的做用域,由於會比其餘函數佔用更多內容,過分使用閉包,會致使內存佔用過多。[[2]]性能

咱們再來個有趣的例子

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

add5和add10都是閉包,也共享函數的定義,可是保存了不一樣的詞法環境,在add5中x=5而在add10中x爲10

內存泄露問題

閉包函數引用外層的變量,當執行完外層函數是,變量會沒法釋放

function  showId() {
    var el = document.getElementById("app")
    el.onclick = function(){
      aler(el.id)   // 這樣會致使閉包引用外層的el,當執行完showId後,el沒法釋放
    }
}

// 改爲下面function  showId() {
    var el = document.getElementById("app")
    var id  = el.id
    el.onclick = function(){
      aler(id)   // 這樣會致使閉包引用外層的el,當執行完showId後,el沒法釋放
    }
    el = null    // 主動釋放el
}
function  factorial(num) {
   if(num<= 1) {
       return 1;
   } else {
      return num * factorial(num-1)
   }}var anotherFactorial = factorial
factorial = nullanotherFactorial(4)   // 報錯 ,由於最好是return num* arguments.callee(num-1),arguments.callee指向當前執行函數,可是在嚴格模式下不能使用該屬性也會報錯,因此藉助閉包來實現


// 使用閉包實現遞歸function newFactorial = (function f(num){
    if(num<1) {return 1}
    else {
       return num* f(num-1)
    }
}) //這樣就沒有問題了,實際上起做用的是閉包函數f,而不是外面的函數newFactorial

用閉包解決遞歸調用問題

用閉包模擬私有方法

編程語言中,好比 Java,是支持將方法聲明爲私有的,即它們只能被同一個類中的其它方法所調用。

而 JavaScript 沒有這種原生支持,但咱們可使用閉包來模擬私有方法。私有方法不只僅有利於限制對代碼的訪問:還提供了管理全局命名空間的強大能力,避免非核心的方法弄亂了代碼的公共接口部分。

下面的示例展示瞭如何使用閉包來定義公共函數,並令其能夠訪問私有函數和變量。這個方式也稱爲 模塊模式(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 */

在以前的示例中,每一個閉包都有它本身的詞法環境;而此次咱們只建立了一個詞法環境,爲三個函數所共享:Counter.increment,Counter.decrement 和 Counter.value。

該共享環境建立於一個當即執行的匿名函數體內。這個環境中包含兩個私有項:名爲 privateCounter 的變量和名爲 changeBy 的函數。這兩項都沒法在這個匿名函數外部直接訪問。必須經過匿名函數返回的三個公共函數訪問。

這三個公共函數是共享同一個環境的閉包。多虧 JavaScript 的詞法做用域,它們均可以訪問 privateCounter 變量和 changeBy 函數。

你應該注意到咱們定義了一個匿名函數,用於建立一個計數器。咱們當即執行了這個匿名函數,並將他的值賦給了變量Counter。咱們能夠把這個函數儲存在另一個變量makeCounter中,並用他來建立多個計數器。
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();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */

請注意兩個計數器 Counter1 和 Counter2 是如何維護它們各自的獨立性的。每一個閉包都是引用本身詞法做用域內的變量 privateCounter 。

每次調用其中一個計數器時,經過改變這個變量的值,會改變這個閉包的詞法環境。然而在一個閉包內對變量的修改,不會影響到另一個閉包中的變量。

以這種方式使用閉包,提供了許多與面向對象編程相關的好處 —— 特別是數據隱藏和封裝。

在循環中使用閉包

<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();

看到這裏你必定能想到,因爲共享了同一個詞法做用域,最終結果是全部的item.help都指向了helptext的最後一項,解決方法是使用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++) {
    (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;
};
相關文章
相關標籤/搜索