一篇文章圖文並茂地帶你輕松學完 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. 創建當前環境下的作用域鏈

    這里作用域鏈顯然是 當前環境下的變量(還沒初始化)以及父親作用域(這里面包括了 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);


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM