從 ECMAScript 來理解閉包與作用域的原理

這一次真的懂了。

簡述

讀這篇文章前,請先理解什麼是 EC 與 VO,不然你絕對看不懂,可以參考這篇:學 hoisting 之前先理解 EC 是什麼?

Scope chain 到底是怎麼產生的?

節錄幾個 ECMAScript 裡提到的重點:

  1. 每一個 EC 都有一個 scope chain,而 scope chain 是一個用陣列包住的各種物件。例如說:[scopeA, scopeB, scopeC, ...]

Every execution context has associated with it a scope chain. A scope chain is a list of objects that are searched when evaluating an Identifier.

  1. 進入一個 EC 時,就會建立一個 scope chain

When control enters an execution context, a scope chain is created and populated with an initial set of objects,

  1. Function 的 scope chain 初始值是由 AO(Activation Object)和 [[Scope]] 組成的

The scope chain is initialised to contain the activation object followed by the objects in the scope chain stored in the [[Scope]] property of the Function object.

所以總結一下,當進入一個 function 時,會產生一個 EC,EC 裡面會有 scope chain ,而 scope chain 的值是自己的 AO/VO 加上 [[Scope]] 來組成,所以大概會長的像這樣:

1
2
3
4
FuncEC: {
AO: {...},
scope chain : [AO, [[Scope]]]
}

至於 AO 是什麼?

AO 其實就跟 學 hoisting 之前先理解 EC 是什麼? 裡面提到的 VO 是 87 分像的東西,差別在於 AO 只會在 function 的 EC 產生, VO 只會在 Global EC 產生。

不過說實在它們的差異很微小,所以呢,把 AO 想成是 VO 就好了。

接下來用一段程式碼示範一下,為了方便理解,請先記住下面的遊戲規則:

  1. [[Scope]] = 上一個 EC 裡的 scope chain(這個比較難解釋,建議搭配下面的例子多想幾遍)
  2. scope chain = 自己 EC 裡的 AO/VO 加上 [[Scope]]
1
2
3
4
5
6
7
8
9
10
11
var a = 1
function yoyoyo() {
var b = 2
function hahaha() {
var c = 3
console.log(b)
console.log(a)
}
hahaha()
}
yoyoyo()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 3. 建立 hahaha 的 EC
hahahaEC: {
AO: {
c: undefined,
}
// 自己的 AO/VO 再加上 [[Scope]]
scope chain:
= [hahahaEC.AO, hahaha.[[Scope]]]
= [hahahaEC.AO, yoyoyoEC.AO, globalEC.VO]
};

// 2. 建立 yoyoyo 的 EC
yoyoyoEC: {
AO: {
b: 2,
hahaha: function
};
// 自己的 AO/VO 再加上 [[Scope]]
scope chain:
= [yoyoyoEC.AO, yoyoyo.[[Scope]]]
= [yoyoyoEC.AO, globalEC.VO]
};
// 初始化 hahaha 的 [[Scope]]
// 也就是 yoyoyo 的 scope chain(上一個 EC 裡的 scope chain)
hahaha.[[Scope]] = [yoyoyoEC.AO, globalEC.VO]


// 1. 建立 globalEC
globalEC: {
VO: {
a: 1,
yoyoyo: function
};
// scope chain = 自己的 AO/VO 再加上 [[Scope]],
// 但 global 不是 function 所以沒有 [[Scope]]
scope chain = [globalEC.VO];
};

// 同時會建立 yoyoyo 的隱藏屬性 [[Scope]]
// 初始值就是 globalEC 裡的 scope chain(上一個 EC 裡的 scope chain)
yoyoyo.[[Scope]] = [globalEC.VO]

所以 scope chain 就是透過上面的方式來建立的。

理解閉包的最後一塊拼圖:假裝自己是 JS 引擎

這個步驟雖然比較繁瑣,但你只要走過一遍,就能理解閉包的原理了。

這邊的例子如下:

1
2
3
4
5
6
7
8
9
10
var v1 = 10
function test() {
var vTest = 20
function inner() {
console.log(vTest, v1)
}
return inner
}
var inner = test()
inner()

接下來就來一行一行執行。

首先,先進入 globalEC:

1
2
3
4
5
6
7
8
9
10
globalEC: {
VO: {
v1: undefined,
inner: undefined,
test: function
}
scope chain: [globalEC.VO]
};
// 初始化 test 的 [[Scope]]
test.[[Scope]] = [globalEC.VO]

Line1 var v1 = 10

1
2
3
4
5
6
7
8
globalEC: {
VO: {
v1: 10, // 更新 VO 裡的值
inner: undefined,
test: function
}
scope chain: [globalEC.VO]
}

Line9 var inner = test()

接下來會進入 test 的 EC:

1
2
3
4
5
6
7
8
9
testEC: {
AO: {
vTest: undefined,
inner: function
}
scope chain: [testEC.AO, globalEC.VO]
};
// 初始化 inner 的 [[Scope]]
inner.[[Scope]] = [testEC.AO, globalEC.VO]

Line3 var vTest = 20

1
2
3
4
5
6
7
testEC: {
AO: {
vTest: 20, // 更新 AO 裡的值
inner: function
}
scope chain: [testEC.AO, globalEC.VO]
}

Line7 return inner

執行到這一行時,照理說 testEC 就會被 JS 的 GC(Garbage Collection 垃圾回收)機制給回收掉。

但是,注意這邊是回傳,所以 inner.[[Scope]] 會被留下來,留下的內容就是 [testEC.AO, globalEC.VO],因此在 inner 這個 function 裡就能存取到 test 跟 global 的 AO/VO。

所以這段就是閉包的原理,因為 return 的關係,讓原本應該要被清掉的資源沒有被清掉。這也解釋了為什麼 inner 明明離開了原本的 EC 卻還有辦法存取到 test 和 global 的 AO/VO。這一段比較複雜一點,你可以多想幾次看看。

總之呢,在回傳後的狀態會變成這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 此時 testEC 已經清掉了。
// 但因為 inner.[[Scope]] 的關係 testEC.AO 會留下
testEC.AO {
vTest: 20,
inner: function
};
globalEC: {
VO: {
v1: 10,
inner: undefined,
test: function
}
scope chain: [globalEC.VO]
}
inner.[[Scope]] = [testEC.AO, globalEC.VO]

Line10 inner()

進入 inner 的 EC:

1
2
3
4
5
6
innerEC: {
AO: {
// 空的
}
scope chain: [innerEC.AO, testEC.AO, globalEC.VO]
}

Line5 console.log(vTest, v1)

這邊分兩段,先找 vTest 的值:

  1. innerEC.AO 找,沒有找到。
  2. testEC.AO 找,找到了,是 vTest: 20,成功印出 20。

接著找 v1 的值:

  1. innerEC.AO 找,沒有找到。
  2. testEC.AO 找,沒有找到。
  3. globalEC.Vo 找,找到了,是 v1: 10,成功印出 10。

以上。

閉包的應用-cache 機制 最容易搞錯的 Scope
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×