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

Huro~發表於2021-02-08

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. 建立當前環境下的作用域鏈

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

  2. 建立當前環境下的變數

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

  3. 設定 this 是誰

    由於沒有明確指定是誰呼叫 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);

相關文章