1. 程式人生 > >js中閉包

js中閉包

閉包是函式和宣告該函式的詞法環境的組合。

詞法作用域

考慮如下情況:

function init() {
    var name = "Mozilla"; // name 是一個被 init 建立的區域性變數
    function displayName() { // displayName() 是內部函式,一個閉包
        alert(name); // 使用了父函式中宣告的變數
    }
    displayName();
}
init();

init() 建立了一個區域性變數 name 和一個名為 displayName() 的函式。displayName() 是定義在 init() 裡的內部函式,僅在該函式體內可用。displayName()

 內沒有自己的區域性變數,然而它可以訪問到外部函式的變數,所以 displayName() 可以使用父函式 init() 中宣告的變數 name 。但是,如果有同名變數 name 在 displayName() 中被定義,則會使用的 displayName() 中定義的 name 。

執行程式碼可以發現 displayName() 內的 alert() 語句成功的顯示了在其父函式中宣告的 name 變數的值。這個詞法作用域的例子介紹了引擎是如何解析函式巢狀中的變數的。詞法作用域中使用的域,是變數在程式碼中宣告的位置所決定的。巢狀的函式可以訪問在其外部宣告的變數。

閉包

現在來考慮如下例子 :

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

執行這段程式碼和之前的 init() 示例的效果完全一樣。其中的不同 — 也是有意思的地方 — 在於內部函式 displayName() 在執行前,被外部函式返回。

第一眼看上去,也許不能直觀的看出這段程式碼能夠正常執行。在一些程式語言中,函式中的區域性變數僅在函式的執行期間可用。一旦 makeFunc()

 執行完畢,我們會認為 name 變數將不能被訪問。然而,因為程式碼執行的沒問題,所以很顯然在 JavaScript 中並不是這樣的。

這個謎題的答案是,JavaScript中的函式會形成閉包。 閉包是由函式以及建立該函式的詞法環境組合而成。這個環境包含了這個閉包建立時所能訪問的所有區域性變數。在我們的例子中,myFunc 是執行 makeFunc 時建立的 displayName 函式例項的引用,而 displayName 例項仍可訪問其詞法作用域中的變數,即可以訪問到 name 。由此,當 myFunc 被呼叫時,name 仍可被訪問,其值 Mozilla 就被傳遞到alert中。

下面是一個更有意思的示例 — 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。

實用的閉包

閉包很有用,因為他允許將函式與其所操作的某些資料(環境)關聯起來。這顯然類似於面向物件程式設計。在面向物件程式設計中,物件允許我們將某些資料(物件的屬性)與一個或者多個方法相關聯。

因此,通常你使用只有一個方法的物件的地方,都可以使用閉包。

在 Web 中,你想要這樣做的情況特別常見。大部分我們所寫的 JavaScript 程式碼都是基於事件的 — 定義某種行為,然後將其新增到使用者觸發的事件之上(比如點選或者按鍵)。我們的程式碼通常作為回撥:為響應事件而執行的函式。

假如,我們想在頁面上新增一些可以調整字號的按鈕。一種方法是以畫素為單位指定 body 元素的 font-size,然後通過相對的 em 單位設定頁面中其它元素(例如header)的字號:

body {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 12px;
}

h1 {
  font-size: 1.5em;
}

h2 {
  font-size: 1.2em;
}

我們的文字尺寸調整按鈕可以修改 body 元素的 font-size 屬性,由於我們使用相對單位,頁面中的其它元素也會相應地調整。

以下是 JavaScript:

function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

size12size14 和 size16 三個函式將分別把 body 文字調整為 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>

用閉包模擬私有方法

程式語言中,比如 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.