先理解作用域跟回傳值,再來談閉包。

突然有一點新的想法。

前言

上個月我有寫過一篇:談談 JS 中的 Closure(閉包),但最近又有了一些新想法,所以想在寫一次關於「閉包(Closure)」這個主題。

就跟標題一樣,我覺得閉包沒有那麼複雜,你只要先理解「作用域」、「作用域鏈」跟「回傳值」這三樣東西,我覺得就沒那麼複雜了。

區域作用域與全域作用域

簡單來說,作用域就是:一個變數的生存範圍

在 JavaScript 中,必須透過 function 才能建立出作用域:

1
2
3
4
5
6
function scope() {
var a = 10
console.log(a)
}
scope() // 10
console.log(a) // ReferenceError: a is not defined

這時候我們會說 scope 產生了一個「區域作用域」,而 a 是這個作用域中的「區域變數」。它只有在 scope 中可以被存取,外面沒辦法。所以 a 的生存範圍只在 scope 這個作用域裡面。

那全域作用域呢?很簡單,就是沒有宣告在 function 裡的變數:

1
2
3
4
5
6
var a = 10
function scope() {
console.log(a)
}
scope() // 10
console.log(a) // 10

這時候在哪裡都能存取到 a,因為 a 是「全域變數」,也就是生存範圍是「全域作用域」。

作用域鏈(Scope chain)

在理解區域作用域跟全域作用域後,要來理解什麼叫「作用域鏈」,舉個簡單的例子:

1
2
3
4
5
6
7
8
9
10
function scopeA() {
var a = 'a'
function scopeB() {
var b = 'b'
console.log(a) // a
}
scopeB()
console.log(b) // ReferenceError: a is not defined
}
scopeA()

在這個例子裡面,我們有兩個作用域,分別是 scopeAscopeB

先看 b 的部分,依照前面提的概念,b 的生存範圍只在 scopeB 中,所以只要出了 scopeB,它就會 GG,所以 scopeA 去存取 b 的時候才會得到錯誤。

a 呢? a 的部分其實就是作用域鏈的機制scopeB 在自己的作用域中找不到 a,所以它就往上找,找到 scopeA 裡面的 a,最後印出 a 的值。

這個「往上找」的行為就叫做「作用域鏈」,唯一要注意的一點是,它只能往上找,不能往下找。

關於回傳值

其實大部分的文章在告訴你作用域跟作用域鏈後,就會開始講什麼是閉包了。但是在那之前,我覺得還有一個很重要的東西不能忽掉:回傳值

一樣來舉點例子,按照作用域的概念,你沒辦法在全域空間中去存取區域變數,但如果真的要存取的話沒有任何辦法嗎?其實是有的,而且你一定也用過,那個方法就是透過「回傳值」:

1
2
3
4
5
6
function scope() {
var a = 10
return a
}
var result = scope()
console.log(result) // 10

這樣就可以拿到 a 的值了。而且不只是這樣,這裡還可以試著回傳物件或陣列看看:

1
2
3
4
5
6
function scope() {
var a = 10
return [a]
}
var arr = scope()
console.log(arr) // [10]
1
2
3
4
5
6
function scope() {
var a = 10
return {a: a}
}
var obj = scope()
console.log(obj) // {a: 10}

最後如果變成回傳 function 呢?

1
2
3
4
5
6
7
8
function scope() {
var a = 10
return function () {
console.log(a)
}
}
var fn = scope() // 拿到 function
fn() // 執行 function => 10

把 function 放到最後才講是因為我覺得這樣比較好理解,如果一開始就拿 function 來舉例你可能會有點混亂,不知道為什麼可以在 function 裡面存取到 a。因此才用這種一步一步來的方式,希望這樣會好理解一點。

所以你知道只要利用「回傳」,不管最後的回傳值是變數值、物件、陣列或函式, a 的值都會被保留下來,只不過 function 的例子比較特別一點,你要考慮前面提到的「作用域鏈機制」。還記得嗎?雖然 function 自己的作用域裡面沒有 a 但是它可以往上找,找到 scope 裡面的 a,所以最後在執行 fn 的時候才能夠存取到 a

靜態作用域與動態作用域

不過你可能還有一個疑問:「fn 是在全域空間執行的,如果現在有個全域變數也叫 a 呢?」

1
2
3
4
5
6
7
8
9
var a = 100 // => 新增一個全域變數
function scope() {
var a = 10
return function () {
console.log(a)
}
}
var fn = scope() //
fn() // 10 or 100 ?

這個問題非常好,首先正確答案是 10

其實是這樣的,在 JavaScript 中一個 function 被宣告的時候,它的作用域就已經決定好了(特別強調 JavaScript 是因為有些程式語言不是這樣子)

所以按照這個規則,function 在被回傳的時候,它的作用域是這樣子:

1
2
3
4
5
6
7
var a = 100
function scope() {
var a = 10
return function () {
console.log(a)
}
}

這樣應該就好理解多了吧?所以不論你之後在哪裡呼叫這個 function,它的作用域都不會變,這一定要搞清楚。

閉包

終於要來談今天的主題,但其實剛剛示範的例子裡就包含了閉包的概念:

1
2
3
4
5
6
7
8
9
function scope() {
var a = 10
return function () {
console.log(a)
}
}
var fn = scope()
fn() // 10
console.log(a) // ReferenceError: a is not defined

不用懷疑,像 a 這樣被關在 function 裡面,只能透過 fn 來存取的這個行為就叫「閉包」。

我覺得這個機制本身沒有什麼,重要的是你要理解這是怎麼做到的,除了前面提到的「回傳值」外,還要理解「作用域」、「作用域鏈」才能真正理解原因。

所以接下來只示範一些閉包的經典題目,還有實際用途。

setTimeout 印出 i 的值

一個很經典的題目:

1
2
3
4
5
for(var i=0; i<=5; i++) {
setTimeout(function() {
console.log(i)
})
}

最後的結果會是:

1
2
3
4
5
6
6
6
6
6

其實原因只是當 callback function 執行的時候,它自己的作用域裡面沒有 i,所以依照作用域鏈往上找,最後找到全域空間的 i,只是這時候迴圈已經跑完了,所以才會是 6

改寫的方式有很多,我列幾個比較常見的。

1. 在包一層 function

1
2
3
4
5
6
7
8
function printNumber(num) {
return function () {
console.log(num)
}
}
for(let i=0; i<=5; i++) {
setTimeout(printNumber(i), 1000)
}

或改寫成 IIFE:

1
2
3
4
5
6
7
for(let i=0; i<=5; i++) {
setTimeout((function(num){
return function() {
console.log(num)
}
})(i), 1000)
}

2. 改用 ES6 的 let

let 的作用域是用 {} 來產生的,所以可以寫成:

1
2
3
4
5
for(let i=0; i<=5; i++) {
setTimeout(function() {
console.log(i)
})
}

可以想成是這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
let i = 0
setTimeout(function() {
console.log(i)
})
}

{
let i = 1
setTimeout(function() {
console.log(i)
})
}

...

建立私有變數及公開方法

透過閉包的特性,你可以把變數藏在 function 裡面,讓外面的人沒辦法存取,並且只能透過公開(return)出去的東西來存取。

來簡單時做一個「開戶」的功能:

  • 開戶時要先存 1000 塊
  • 開戶後可以查詢餘額、領錢、存錢
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
function createBank(money) {
if (money < 1000) return '存太少啦'
// 要公開出去的東西
let bank = {
searchMoney() {
console.log('目前餘額:', money)
},
getMoney(value) {
money -= value
console.log('存入金額:', value)
console.log('目前餘額:', money)
},
setMoney(value) {
money += value
console.log('領取金額:', value)
console.log('目前餘額:', money)
}
}
console.log('開戶成功')
return bank
}
const error = createBank(900)
console.log(error) // 存太少啦
const PeaNu_BANK = createBank(1000) // 開戶成功
PeaNu_BANK.searchMoney() // 目前餘額:1000
PeaNu_BANK.setMoney(500) // 存入金額:500,目前餘額:1500
PeaNu_BANK.getMoney(1000) // 領取金額:1000,目前餘額:500
console.log(money) // ReferenceError: a is not defined

這邊簡單示範所以就沒做太多的額外處理了,方便看懂最重要。

所以以上這是關於閉包,還有閉包的用處。

mentor-program-day33 正則表達式之「我只想要 xxx 裡面的內容」
Your browser is out-of-date!

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

×