閉包 - 那些前端開發應該要知道的小事(三)

alexianalexian
2019-10-15
那些前端開發應該要知道的小事

前言

開始這個系列的原因,是因為雖然在程式中可能是不影響功能的小事,但卻會關係到整個大系統的流暢度,或是程式碼的簡潔度,這次要提及的是「閉包」(Closure)。

以往尚未完全理解閉包以前,我一直以為閉包只是用於把程式碼包起不外露,實際上閉包很大程度是關於「作用域」的理解與應用,可以說閉包其實是一個大坑,但如果不搞懂它,你的程式就會有很大機會出現作用域存取的Bug了,網路上其實也有很多介紹閉包的文章,但大部分都所以這篇文章會儘量簡單去說明閉包的原理和操作方法,如果有理解錯誤的地方,也希望大家不吝指教。

在最初學習JavaScript的時候,教學都會使用即時調用函式(IIFE)來包裹程式碼,目的是為了避免程式碼受污染,實際上這裡就應用了「閉包」存取作用域的能力,這通常是初學者第一次認知到閉包,但這只是閉包的冰山一角。

/* IIFE例子 */
(function(){

  var a = 'alex';
  console.log(a) // 'alex'

})()

console.log(a) // Uncaught ReferenceError: a is not defined

所以說了這麼久,閉包到底是什麼呢?

閉包定義

實際上,閉包是一種特定的資料結構,是JavaScript函式的一種固有特性,它會決定函式在呼叫時能夠存取的變數範疇,以及在記憶體保留已產生的變數。

閉包與作用域

作用域是程式語言很重要的一個概念,它決定程式中能夠取得的變數範疇,學習過JavaScript都會知道在ES6以前,能夠產生獨立作用域的就只有函式,而閉包讓作用域的變數儲存起來,不被釋放。 假設全域為國家,房子的一家之主是父親,客廳有兩張沙發;而在我則是我房間的主人,有四個玩具在裡面,實現的程式碼為:

var master = 'president';

function house() {
  var master = 'father';
  var sofa = 2;

  (function room() {
    var master = 'me';
    var toys = 4;

    console.log(master); // 'me'
    console.log(toys); // 4
    console.log(sofa); // 2
  })()

  console.log(master); // 'father'
  console.log(toys); // Uncaught ReferenceError
  console.log(sofa); // 2
})()

console.log(master); // 'president'
console.log(toys); // Uncaught ReferenceError
console.log(sofa); // Uncaught ReferenceError

雖然實際上程式碼跑到第二個console.log(toys);時就會報錯,但我已把每個console.log()出現的結果放到註解中,我們可以根據結果作出如下解釋:

無論在全域,house、或是room中,都各自宣告了master這個變數,而在不同區域下,master都有獨立的作用域,所以閉包第一個特性,便是確保上層的變數不被污染。 接下來我們看sofa和toys在各作用域中表現,無論在house和room中,我們都可以取得sofa的數量,然而room中並沒有宣告sofa這個變數,由此我們可以知道在room中取得的sofa變數,是往上層的作用域進行查找的;然而當從house想要取得toys變數時,則會出現錯誤,這便是利用上閉包第二個特性,閉包能夠獨立儲存其中的變數不被外部修改。

閉包與回調函式

閉包經常會應用於回調函式(callback)上,確保變數能夠在函式呼叫後保留,看以下例子:

function hello(msg){
  setTimeout(function() {
    console.log(`hello ${msg}`)
  }, 1000)
}

hello('world'); // hello world

閉包的特性讓setTimeout()執行時,原本傳入的變數hello函式中變數能夠一併保存起來,讓回調函式能順利運作。

閉包避免產生的誤會

通常提及閉包的重要性,都會使用迴圈配合說明,例如我們想要每隔一秒印出該秒數:

for(var i=1; i<=5; i++){
  setTimeout(function(){
    console.log(i);
  }, i * 1000);
}

以上的程式碼結果,只會會部印出6,看起來程式碼沒有問題啊?實際上這個迴圈產生的效果是:

var i = 1;

setTimeout(function(){
  console.log(i);
}, 1 * 1000);

setTimeout(function(){
  console.log(i);
}, 2 * 1000);

//3,4,5,6...

i = 6;

沒有了閉包的作用域,變數i沒有儲存到setTimeout中,要謹記的是,閉包的效果需要被呼叫時才會出現,故上述的寫法並不會把i當前的值記錄下來,要把其修改為閉包的寫法,可以用函式進行包裹:

function count(i){
  setTimeout(function(){
    console.log(i);
  }, i * 1000);
}

for(var i=1; i<=5; i++){
  count(i);
}

我們加入一個count函式,每一次迴圈,i的值便會儲存至呼叫的count函式中,這樣便可以輸出我們想要的結果。

為什麼要使用閉包?

閉包有許多優點,光用說明可能不好解釋,所以我們使用閉包的方式來實現場景。假設我的保險庫有10塊,桌上有5塊,我想把身上的錢存入銀行時,不會影響到桌上的金額數目(我們假設全域為桌子),實現的程式碼為:

var money = 5; // 桌上的錢

function locker(){
  var money = 10; // 保險庫的錢

  //存錢到保險庫的方法,以及顯示保險庫金額的方式,只有保險庫才能提供
  return {
    saveMoney: function (){ money +=1; console.log(money) } ,
    deposit: function () { return money; }
  }
}

var myLocker = locker();

myLocker.saveMoney(); // 11
console.log(money); // 5
money += 1;
console.log(money); // 6
console.log(myLocker.deposit()); //  11

在輸出端可以看到,由於我存錢到保險庫時,只會影響到保險庫內的金額,桌上的金額依然會維持;而把桌上金額增加時,也不會影響到保險庫中的存款。這便是應用上閉包的實際例子,在這個例子我們不難看出閉包的好處,閉包可以保護我們不允許被篡改的資料,除非使用閉包中提供的方法(如上述的locker中的money);另外,也可以保障全域中變數不被污染。

總結

  • 閉包是一種資料結構,也是JavaScript函式特性
  • 閉包能夠保存當中的能取得之變數,並且要在呼叫時才開始作用
  • 閉包可以保護存入的變數不被外部修改

當然閉包並不止如此,這篇文章只能算是瞭解閉包的皮毛,要更深入瞭解,可以查看參考

參考

https://blog.techbridge.cc/2018/12/08/javascript-closure/ https://eyesofkids.gitbooks.io/javascript-start-from-es6/content/part4/closure.html


上一篇

相等比較 - 那些前端開發應該要知道的小事(四)

下一篇

addEventListener的第三個參數 - 那些前端開發應該要知道的小事(二)


Alex Ian
Alex Ian

ReactJS, VueJS 畫畫/寫作/前端開發

Alex Ian
Alex Ian

ReactJS, VueJS 畫畫/寫作/前端開發


共29篇文章
文章分類
  • 前端開發14
  • JavaScript7
  • 那些前端開發應該要知道的小事5
  • Gatsby.js3
  • ChromeExtension2
  • NetlifyCMS2
  • 生產力工具2
  • 關於我1
  • 非Coding1
  • Svelte1
  • raycast1
  • toolRecommend1
  • productivity1
  • MutationObserver1
  • observer pattern1
  • 開發效率1
  • will-change1
  • GitHubCopilot1
  • 生產力1
  • ChatGPT1
  • AI1
  • Copilot1
  • WebAssembly1
  • SEO1
  • GoogleSearchConsole1
  • react1
  • applescript1
  • 閱讀1
  • css1
  • grid1
  • micro-frontend1

Build with GatsbyJS and React 18.2.0. Hosted on Netlify.

The original code is open source and available at calpa/gatsby-starter-calpa-blog

Copyright ©AlexIan's blog 2025.

  1. 從頭開始上架 Chrome extension

    上回提到我們在本地端建好一個開發版本的 Chrome …
  2. Gatsby 部落格更新記錄 1.0

    首先把 Blog 連結貼在最前面:https://alex-ian.me/ 許多工程師 …
  3. Debounce & Throttle - 那些前端開發應該要知道的小事(一)

    前言 也許一開始接觸前端開發的新手們,都有使 …
  4. 使用 GitHub Copilot 一個月心得

    後悔自己為何沒有早點課金 在 Modern Web Conference 2023 觀後感 …
  5. 什麼是 Headless CMS?以探索 Netlify CMS 為例

    小弟的部落格使用 Gatsby.js 框架來架設,作為一個靜 …
  6. Modern Web Conference 2023 觀後感

    https://modernweb.tw/ 前言 Modern Web Conference (MWC) 作為每年度經典的網頁開 …
  7. 什麼是語法糖(Syntactic sugar)? - 2024 你要知道的 JS 語法糖 🍭

    語法糖能吃嗎? 在算數學時,不知道你什麼時候開 …
  8. 閉包 - 那些前端開發應該要知道的小事(三)

    前言 開始這個系列的原因,是因為雖然在程式中 …
  9. Raycast — Mac 提升工作效率的啟動工具

    Mac 的使用者,應該對 Spotlight 這個系統預設的啟動工具 …
  10. 使用 Notion 管理我的年度目標 暨 2023 回顧 & 2024 展望

    都進入 2024 才來展望 想要成為流量的奴隸,自然要 …
  11. 慢半拍來初嚐 Svelte

    Svelte 都出來兩年多的框架現在才來玩,有夠慢半拍 …
  12. 從頭開始學習開發 Chrome extension (v3 版本)

    Chrome extension 經歷過 v1, v2 後,來到了 v3,雖然曾小量接觸 v2 …
  13. 留言簿

    歡迎留言
  14. addEventListener的第三個參數 - 那些前端開發應該要知道的小事(二)

    前言 開始這個系列的原因,是因為雖然在程式中 …
  15. 在中大型企業下微前端的實現與挑戰

    我們先聊工程面,人的部分先不要 小弟任職前端 …
  16. will-change — 那些前端開發應該要知道的小事(七)

    你要做動畫你要先說啊 前端開發少不免會需要 …
  17. 關於我

    關於我 我是Alex Ian,是個小小的前端工程師,出來工 …
  18. 開發效率提升 - 操作鍵盤 & 快捷鍵篇

    我國中的電腦課,會教導學生倉頡和速成輸入法 …
  19. macbook-pro(retina) late2012 換電池記錄

    這篇文章其實與Coding無關,主要是記錄mbp換電池的 …
  20. polyfill是什麼?

    人在江湖,身不由己,在接案的過程中,總有一些你 …
所有結果