js閉包探祕

譯者:閉包都被討論爛了,不理解閉包都很差意思說本身會js,但我看到這篇文章仍是感受眼前一亮,也讓我對閉包有了一些新的理解,而且涉及了一些類和原型鏈的知識,這是一篇2012年的文章,稍微有點早,內容也略微基礎,可是很明晰,但願能給讀者帶來新的理解。

閉包(Closure) 是javascript這門語言中有些複雜而且充滿誤解的特性。簡言之,閉包是一個對象,這個對象包含一個方法(function)和該方法建立時環境的引用(reference to the enviroment)。爲了徹底理解閉包,咱們還須要理解兩個js中的特性,一個是一級方法(first-class function),另外一個是內部方法(inner function)。javascript

一級方法/First-Class Functions

在js中,方法是頭等公民,由於它能夠被輕易轉換成其餘數據類型。好比,一級方法能夠實時構建而且賦值給一個變量。也能夠傳遞給其餘方法,或者經過其餘方法返回。除了知足這些標準之外,方法也擁有本身的屬性和方法。
經過下述例子,咱們來看一下一級方法的能力。html

var foo = function() {
  alert("Hello World!");
};

var bar = function(arg) {
  return arg;
};

bar(foo)();
譯者注:省略原文對代碼的文字解釋,這裏體現的是一級方法能夠返回參數,參數能夠是另一個一級函數,返回的結果還能夠調用。

內部方法/Inner Functions

內部方法或者說嵌套方法,是指定義在其餘方法內部的方法,每當外部方法被喚起,內部方法的實例就被建立。下面的例子反應內部方法的使用,add方法是外部方法,doAdd是內部方法。java

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

內部方法獲取外部方法的做用域,便造成了一個閉包。典型的場景是外部函數將其內部方法返回,內部方法保持了外部環境的引用,並保存了做用域下的全部變量。
一下例子展現閉包如何建立並使用。dom

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;
}

什麼時候使用閉包?

閉包能夠實現不少功能。好比將回調函數綁定指定參數。咱們說兩個讓你的生活和開發變得更簡單的場景。this

  1. 配合定時器

閉包結合setTimeout和setInterval很是有用,閉包容許你向回調函數傳入指定參數,好比下面的例子,每秒鐘在給指定dom插入字符串。spa

<!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()),解決這個問題,能夠經過閉包將指望值綁定於回調函數裏,咱們能夠改寫如上代碼:prototype

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不是純正的面向對象的語言,所以也沒有私有屬性的概念。不過,咱們能夠經過閉包來模擬私有屬性。回想一下,閉包包含了一份其建立環境的引用,這份引用已經不在當前做用域中了,所以這份引用只能在閉包中訪問,這本質上就是私有屬性。
看以下例子(譯者:省略對代碼的文字描述):code

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!");
};

須要記得一些事情

  • 閉包包含了一個方法,以及建立它的代碼環境引用
  • 閉包會在外部函數包含內部函數的狀況下造成
  • 閉包能夠輕鬆的幫助回調函數傳入參數
  • 類的私有屬性能夠經過閉包模擬
  • 類的構造器中使用閉包不是一個好主意,將它們放到原型鏈中
相關文章
相關標籤/搜索