一篇文章圖文並茂地帶你輕鬆學完 JavaScript 閉包
阿新 • • 發佈: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)
擁有了這些東西后,我們才可以分配記憶體,起到一個準備的作用。
我們用下述程式碼加深對執行上下文的理解
```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);
```