自學日記

學習程式相關知識及學習法

0%

JS 什麼是閉包(Closure)

什麼是閉包 (Closure)

什麼是閉包及範圍鏈

在閱讀本文章時,須了解全域變數、區域變數及作用域,在談閉包之前,我們再談一個觀念,所謂的範圍鏈(Scope Chain)。

範圍鏈 (Scope Chain)

“內層的 function inner 可以讀取外層宣告的變數,但外層的 outer function 存取不到內層宣告的變數。 若是在自己層級找不到就會一層一層往外找,直到 Global 為止。”-重新認識 JavaScript: Day 19 閉包 Closure

以下程式碼舉例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function outer() {

var b = a * 2;

function inner(c) {

console.log(a, b, c);
}

inner(b * 3);
}

var a = 1;
outer(a);

console.log(a,b,c)結果為:

原因為:
function內,a沒有被宣告,所以向外層找,再global找到var a=1,因此b也可以進一步得到2的結果,c也同理可得。

閉包(closure)

上方範圍鏈解釋後,我們能理解作用域產生的影響,那為什麼要這樣使用呢?原因在於我們可能會使用內部function做一些重複的運算,讓它利用從外層取值的特性,達到保護function內部不被汙染,就是所謂「閉包」。

舉例例子輔佐,讓觀念更清楚:

閉包基礎版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function closureGo() {
var x = 10;
// function內無x值,因此從外層找x
// 此輸入值可以取為inputName等等,不需要與var名稱同名,
// 這裡取一樣是為方便理解
function closure(輸入值) {
console.log(x + 輸入值)
}
return closure
}
// 為了好理解,將var 名稱 取名為輸入值,
// 其實不需要與內部函數同名,
// 只為好理解為var 輸入值(輸入數值)可以放進函式計算
var 輸入值 = closureGo();

retun寫法我們可以進一步簡化為:

1
2
3
4
5
6
7
function closureGo() {
var x = 10;

return function (輸入值) {
console.log(x + 輸入值)
}
}

我們可以再進一步使用ES6箭頭函式語法簡化為:

1
2
3
4
5
6
function closureGo() {
var x = 10;

return (輸入值) =>
console.log(x + 輸入值)
}

使用檢查介面觀察及輸入參數結果為:

閉包圖示說明

以上方範例function,我們明白閉包中的outer functioninner function,我們利用圖示來說明:

在圖片裡,closure中有看到by reference,準確說明閉包是參照(referce)外部的值。

我們還可以從示範function知道:

  • 閉包的最大特點就是它會記憶函式建立時的環境,也就是內部函式所能存取得到的作用域連鎖中的所有變數當下的值。
  • 依上一點最後,進一步說明函式作用域連鎖規則:內部函式可以看到(或存取得到)外部函式,而形成一個Scope Chain(作用域連鎖),內部函式可以有三個作用域:
  • 在閉包中,可以定義私有變數和函式,外部無法訪問它們,從而做到了私有成員的隱藏和隔離。而通過返回物件或函式,或是將某物件作為引數傳入,在函式體內對該物件進行操作,就可以公開我們所希望對外暴露的公開的方法與資料。-引用自JavaScript 模組化程式設計 - Module Pattern

閉包變化進階題:

1
2
3
4
5
6
7
8
9
10
11
function closureGo(x){
return function 比較大小(輸入值){
if(x > 輸入值)
console.log("大")
else{
console.log("小")
}
}
}
var 輸入值a = closureGo(10)
var 輸入值b = closureGo(20)

再進階-閉包在迴圈中(setTimeout)的作用及IIFE解決方式:

1
2
3
4
5
6
7
8
for (var index = 0; index < 10; index) {
// setTimeout為非同步
setTimeout(function () {
console.log(index)
// js內的1秒要用1000毫秒表示
},1000);

}

產生結果為:

替換var改成let可以解決setTimeout的問題:

但這裡我們換一種方式,改使用IIFE(立即函式)閉包解決:

1
2
3
4
5
6
7
8
for (var index = 0; index < 10; index) {
console.log(index);
(function (index) {
setTimeout(function () {
console.log("setTimeout i:", index)
},1000)
})(index);
}

上方code,console.log(index):

使用立即函式後,產生的console.log("setTimeout i:", index):

模組模式(The Module Pattern)

這個模式在JavaScript 中被稱為模組。最常見的實現模組模式的方法通常被稱為模組暴露,這裡展示的是其變體。我們仔細研究一下這些程式碼。

首先,CoolModule() 只是一個函式,必須要通過呼叫它來建立一個模組例項。如果不執行外部函式,內部作用域和閉包都無法被建立。其次,CoolModule() 返回一個用物件字面量語法{ key: value, … } 來表示的物件。這個返回的物件中含有對內部函式而不是內部資料變數的引用。我們保持內部資料變數是隱藏且私有的狀態。可以將這個物件型別的返回值看作本質上是模組的公共API。這個物件型別的返回值最終被賦值給外部的變數foo,然後就可以通過它來訪問API 中的屬性方法,比如foo.doSomething()。

doSomething() 和doAnother() 函式具有涵蓋模組例項內部作用域的閉包( 通過呼叫CoolModule() 實現)。當通過返回一個含有屬性引用的物件的方式來將函式傳遞到詞法作用域外部時,我們已經創造了可以觀察和實踐閉包的條件。如果要更簡單的描述,模組模式需要具備兩個必要條件。

  1. 必須有外部的封閉函式,該函式必須至少被呼叫一次(每次呼叫都會建立一個新的模組例項)。

  2. 封閉函式必須返回至少一個內部函式,這樣內部函式才能在私有作用域中形成閉包,並且可以訪問或者修改私有的狀態。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function CoolModule() {
    var something = "cool";
    var another = [1, 2, 3];
    function doSomething() {
    alert( something );
    }
    function doAnother() {
    alert( another.join( " ! " ) );
    }
    return {
    doSomething: doSomething,
    doAnother: doAnother
    };
    }
    var foo = CoolModule();
    foo.doSomething(); // cool
    foo.doAnother(); // 1 ! 2 ! 3

引用自JavaScript利用閉包實現模組化