一篇文章圖文並茂地帶你輕鬆學完 JavaScript 閉包

JavaScript 閉包

為了更好地理解 JavaScript 閉包,筆者將先從 JavaScript 執行上下文以及 JavaScript 作用域開始寫起,如果讀者對這方面已經了解了,可以直接跳過。

1. 執行上下文

簡單來說,JavaScript 有三種程式碼運行環境,分別是:

  1. Global Code 是 JavaScript 程式碼開始運行的默認環境
  2. Function Code 是 JavaScript 函數運行的環境
  3. Eval Code 是 利用 eval 函數執行的程式碼環境

執行上下文可以理解為上述為了執行對應的程式碼而創建的環境。

例如在第一個環境執行前,我們需要考慮

  1. 該環境下的所有變數對象

    例如用 let const var 定義的變數,或者是函數聲明,函數參數 arguments

  2. 該環境下的作用域鏈

    包括 該環境下的所用變數對象 以及父親作用域 (我們當然可以用到父親作用域提供的函數和變數

  3. 是誰執行了這個環境 (this)

擁有了這些東西後,我們才可以分配記憶體,起到一個準備的作用。

我們用下述程式碼加深對執行上下文的理解

let global = 1;

function getAgeByName(name) {
    let xxx = 1;
    function age() {
        console.log(this);
        const age = 10;
        if (name === "huro")
            return age;
        else
            return age * 10;
    }
    return age();
}

假設我們執行 age 函數

  1. 創建當前環境下的作用域鏈
  2. 創建當前環境下的變數
  3. 設置 this 是誰

這裡作用域鏈顯然是 當前環境下的變數(還沒初始化)以及父親作用域(這裡面包括了 global 變數以及 xxx 變數, name 形參)等,這些我們當然都可以在 age 中使用。

當前環境下的變數包括接收到的形參 arguments age 變數

由於沒有明確指定是誰調用 age 方法,因此 this 在瀏覽器環境下設置為 window

在創建好環境進行變數的搜索的時候,會先搜索當前環境下的變數,如果沒有隨著作用域鏈往上搜索。

由於 ES6 箭頭函數本身不創建 this 因此他會向上尋找 this

上述提到了作用域,作用域也分幾種

作用域

  1. 塊級作用域

    在很多語言的規範里經常告訴我們,如果你需要一個變數再去定義,但是如果你使用 JavaScriptvar 定義變數,你最好別這麼干。最好是都定義在頭部。

    因為 var 沒有塊級作用域

if (true) {
    var name = "huro";
}
console.log(name); // huro

​ 不過當你使用 letconst 定義的話,就不存在這樣的問題。

if (true) {
    let name = "huro";
}
console.log(name); // name is not defined
  1. 函數和全局作用域

    這個和大部分語言是一致的。

let a = 1;
function fn() {
    let a = 2;
    console.log(a); // 2
}

閉包

擁有了作用域和作用域鏈,內部函數可以訪問定義他們的外部函數的參數和變數,這非常好。

如果我們希望一個對象不被外界更改(污染)

const myObject = () => {
    let value = 1;
    return {
        increment: (inc) => {
            value += inc;
        }
        getValue: () => {
            return value;
        }
    }
}

由於外界不可能直接訪問到 value 因此就不可能修改他。

利用閉包

在構造函數中,對象的屬性都是可見的,沒法得到私有變數和私有函數。一些不知情的程式設計師接受了一種偽裝私有的模式。

例如

function Person() {
    this.________name = "huro";
}

用於保護這個屬性,並且希望使用程式碼的用戶假裝看不到這種奇怪的成員元素,但是其實編譯器並不知情,仍會在你輸入 xxx.__ 的時候提示你有 xxx.________name 屬性

利用閉包可以很輕易的解決這個問題。

function Person(spec) {
    let { name } = spec;
   
    this.getName = () => {
        return name;
    }
    this.setName = (name) => {
        name = "huro";
    }
    return this;
}
const p = new Person({ name: "huro" });
console.log(p.name) // undefined
console.log(p.getName()) // "huro"

注意閉包帶來的問題

<body>
    <div class="name">
       	huro
    </div>
    <div class="name">
        lero
    </div>
</body>
const addHandlers = (nodes) => {
    let i ;
    for (i = 0; i < nodes.length; i += 1) {
        nodes[i].addEventListener("click", () => {
            alert(i); // 總是 nodes.length
        })
    }
}
const doms = document.getElementsByClassName("name");
addHandlers(doms);

你會發現,列印出來的結果總是 2,這是作用域的原因,由於 i 是父作用域鏈的變數,當向上查找的時候,i 已經變成 2 了。

正確的寫法應該是

const addHandlers = (nodes) => {
    for (let i = 0; i < nodes.length; i += 1) {
        nodes[i].addEventListener("click", () => {
            alert(i);
        })
    }
}
const doms = document.getElementsByClassName("name");
addHandlers(doms);