在JavaScript中,閉包(Closure)與作用域鏈(Scope Chain)的關係非常重要,在往下看之前建議可以先複習一下「【day3】(JavaScript) 作用域 Scope」的文章~
快速複習一下作用域鏈
var globalVar = "哇系全域";
function outerFunction() {
var outerVar = "哇系外層";
function innerFunction() {
var innerVar = "哇系內層";
console.log(innerVar); // 輸出 "哇係內層"
console.log(outerVar); // 輸出 "哇係外層"
console.log(globalVar); // 輸出 "哇係全域"
}
innerFunction();
}
outerFunction();
在innerFunction
中可以訪問到外層宣告的變數,像是outerVar
和globalVar
,但是在外層的outerFunction
訪問不到內層的innerVar
。
當在自己層級找不到變數時,則會一層一層往外找,直到global為止。
這個行為即是作用域鏈(Scope Chain)
有了作用域鏈的概念後,再舉例一段程式碼看看什麼是閉包(Closure)
function makeFunc() {
const name = "名字";
function b() {
return name
}
return b;
}
const a = makeFunc();
a();
makeFunc
的函式,函式內部有name
變數以及內層函式b
b
因為 作用域鏈(Scope Chain) 的關係可以訪問到name
變數,並且當b
函式被調用時會返回name
的值「"名字"」makeFunc
函式的返回值是b
。(這邊返回的是函式本身,不是函式結果)a
變數將makeFunc()
賦值給它,當我們呼叫a
變數時makeFunc()會被調用
,然後返回b
函式因此可以簡單整理出閉包(Closure)是什麼閉包就是內部函式可以透過作用域取得函式外部的變數,並且記住這個變數,因此閉包很常被用來做狀態保存。
謎之音:理論上來看,是不是function就都算是閉包呢?是沒有錯的
對於這個議題更詳盡的探討,推薦Huli的這篇文章「所有的函式都是閉包:談 JS 中的作用域與 Closure」
再來寫一個例子~
function counter(){
let count = 0
function innerFunc(){
return ++count
}
return innerFunc
}
const aCounter = counter()
const bCounter = counter()
console.log(aCounter()) // 輸出 1
console.log(aCounter()) // 輸出 2
console.log(bCounter()) // 輸出 1
透過閉包(Closure)的方式來建立counter
函式,內層的innerFunc
函式透過(Scope Chain)返回count
的值,最後由counter
函式返回innerFunc
。
這樣子也就可以讓其他的外層儲存到裡面的變數,並且aCounter
與bCounter
也不會影響到彼此的值,更不需要擔心count會在其他地方被改變。
function a(){
const hugeData = newArray(10000).fill("data")
return function b(){
return hugeData.length
}
}
const closures = []
for(let i = 0; i < 100; i++){
return closures.push(a())
}
在這個範例中可以看到每一個closures
都對hugeData
有參考,即使之後在某些時刻不需要這些資料了,但是它不會被記憶體回收,如果不斷建立這個閉包,會消耗大量的記憶體,可能會讓瀏覽器的產生效能問題。
閉包是 JavaScript 中的一個核心和強大特性,它允許函數記住並訪問其外部作用域的變數,使其在作用域以外的地方也可以被調用。雖然閉包提供了資訊隱藏和數據封裝的能力,但不當使用可能會導致記憶體泄漏和其他問題。因此需要謹慎在適當的情境下使用它ʕง•ᴥ•ʔง
參考資料
所有的函式都是閉包:談 JS 中的作用域與 Closure
JavaScript 進階 - 什麼是閉包?探討 Closure & Scope Chain
MDN