1. 程式人生 > >一篇文章圖文並茂地帶你輕鬆學完 JavaScript 閉包

一篇文章圖文並茂地帶你輕鬆學完 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) 擁有了這些東西后,我們才可以分配記憶體,起到一個準備的作用。 我們用下述程式碼加深對執行上下文的理解 ```js 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. 塊級作用域 在很多語言的規範裡經常告訴我們,如果你需要一個變數再去定義,但是如果你使用 `JavaScript` 的 `var` 定義變數,你最好別這麼幹。最好是都定義在頭部。 因為 `var` 沒有塊級作用域 ```js if (true) { var name = "huro"; } console.log(name); // huro ``` ​ 不過當你使用 `let` 或 `const` 定義的話,就不存在這樣的問題。 ```js if (true) { let name = "huro"; } console.log(name); // name is not defined ``` 2. 函式和全域性作用域 這個和大部分語言是一致的。 ```js let a = 1; function fn() { let a = 2; console.log(a); // 2 } ``` ### 閉包 閉包實質上可以理解為"定義在一個函式內部的函式" 擁有了作用域和作用域鏈,內部函式可以訪問定義他們的外部函式的引數和變數,這非常好。 如果我們希望一個物件不被外界更改(汙染) ```js const myObject = () => { let value = 1; return { increment: (inc) => { value += inc; } getValue: () => { return value; } } } ``` 由於外界不可能直接訪問到 `value` 因此就不可能修改他。 #### 利用閉包 在建構函式中,物件的屬性都是可見的,沒法得到私有變數和私有函式。一些不知情的程式設計師接受了一種偽裝私有的模式。 例如 ```js function Person() { this.________name = "huro"; } ``` 用於保護這個屬性,並且希望使用程式碼的使用者假裝看不到這種奇怪的成員元素,但是其實編譯器並不知情,仍會在你輸入 `xxx.__` 的時候提示你有 `xxx.________name` 屬性 利用閉包可以很輕易的解決這個問題。 ```js 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" ``` #### 注意閉包帶來的問題 ```html huro lero
``` ```js 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` 了。 正確的寫法應該是 ```js 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); ```