JavaScript之閉包

閉包的定義

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

var myFunc = makeFunc();
myFunc();

​ 上面兩段代碼運行結果是完全一樣的。不同的是:makeFunc函數中,內部函數 displayName() 在執行前,被外部函數返回。在一些編程語言中,函數中的局部變量僅在函數的執行期間可用。一旦 makeFunc() 執行完畢,我們會認爲 name 變量將不能被訪問。然而,因爲代碼運行得沒問題,所以很顯然在 JavaScript 中並不是這樣的。

​ JavaScript這樣的原因是:JavaScript中的函數會形成閉包。 閉包是由函數以及創建該函數的詞法環境組合而成,這個環境包括了這個閉包創建時所能訪問的所有局部變量。在上面的例子中,myFunc 是執行 makeFunc 時創建的 displayName 函數實例的引用,而 displayName實例仍可訪問其詞法作用域中的變量,即可以訪問到 name 。由此,當 myFunc 被調用時,name 仍可被訪問,其值 Mozilla 就被傳遞到alert中。

​ 下面看一個更加有趣的例子:

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 求和。

閉包的應用

應用於面向對象編程

​ 閉包很有用,因爲它允許將函數與其所操作的某些數據(環境)關聯起來。這顯然類似於面向對象編程。在面向對象編程中,對象允許我們將某些數據(對象的屬性)與一個或者多個方法相關聯。因此,通常你使用只有一個方法的對象的地方,都可以使用閉包。

應用於Web開發

​ 在 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;
<p>Some paragraph text</p>
<h1>some heading 1 text</h1>
<h2>some heading 2 text</h2>
<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.value()); /* logs 1 */

上面計數器代碼只創建了一個詞法環境,爲三個函數所共享Counter.increment,Counter.decrement 和 Counter.value。該共享環境創建於一個立即執行的匿名函數體內。這個環境中包含兩個私有項:名爲privateCounter 的變量和名爲 changeBy 的函數。這兩項都無法在這個匿名函數外部直接訪問。必須通過匿名函數返回的三個公共函數訪問。這三個公共函數是共享同一個環境的閉包。因爲JavaScript 的詞法作用域,它們都可以訪問 privateCounter 變量和 changeBy 函數。

​ 下面改進一下,定義一個不立即執行的非匿名函數,用於創建計數器。這樣可以創建多個計數器並且互相不影響:

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 。每次調用其中一個計數器時,通過改變這個變量的值,會改變這個閉包的詞法環境。然而在一個閉包內對變量的修改,不會影響到另外一個閉包中的變量。

閉包的問題

問題來源

​ 問題例子引用自廖雪峯JavaScript教程之閉包,在應用閉包時,需要注意一個問題,返回的函數並沒有立刻執行,而是直到調用了f()才執行。我們來看一個例子:

function count() {
    var arr = [];
    for (var i=1; i<=3; i++) {
        arr.push(function () {
            return i * i;
        });
    }
    return arr;
}

var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];

在上面的例子中,每次循環,都創建了一個新的函數,然後,把創建的3個函數都添加到一個Array中返回了。你可能認爲調用f1()f2()f3()結果應該是149,但實際結果是:

f1(); // 16
f2(); // 16
f3(); // 16

全部都是16!原因就在於返回的函數引用了變量i,但它並非立刻執行。等到3個函數都返回時,它們所引用的變量i已經變成了4,因此最終結果爲16

問題解析

​ 首先我們弄懂上面代碼的運行流程:首先var results = count();之後,函數count已經被調用了,所以一次執行函數內的各段代碼:var arr = [];for (var i=1; i<=3; i++),這個for循環尤其值得注意。因爲此時循環體執行了push方法,將一個個函數function () { return i * i;}添加到數組內,但是這個函數並沒有被調用,還只是一個變量,所以for循環依次執行,直到i = 4。因爲閉包,內部函數function () { return i * i;}引用的i就是外部變量,for循環中的i = 4。所以,之後數組arr內的函數的i都是4。

​ 調用函數count後,變量results已經是數組arr了。數組裏面元素依次是function f1() { return i * i;} function f2() { return i * i;} function f3() { return i * i;}。但是三個函數都沒有被調用,直到var f1 = results[0];,此時function f1() { return i * i;}開始執行,如上段所寫,此時的i = 4,所以,返回值就是16了。後面兩個調用也是類似情況。

問題啓示

​ 返回閉包時牢記的一點就是:返回函數不要引用任何循環變量,或者後續會發生變化的變量。如果一定要引用循環變量,方法是再創建一個函數,用該函數的參數綁定循環變量當前的值,無論該循環變量後續如何更改,已綁定到函數參數的值不變:

function count() {
    var arr = [];
    for (var i=1; i<=3; i++) {
        arr.push((function (n) {
            return function () {
                return n * n;
            }
        })(i));
    }
    return arr;
}

var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];

f1(); // 1
f2(); // 4
f3(); // 9

上述代碼中,避免在arr.push方法中,實現了每次循環都立即將當前的參數i傳送給函數,然後立即塞進數組。這樣就避免了i最後才傳給函數。注意,這裏用了一個“創建一個匿名函數並立刻執行”的語法,才能及時綁定參數i

(function (x) {
    return x * x;
})(3); // 9

 原文鏈接:https://www.cnblogs.com/cjvae/p/9786598.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章