iT邦幫忙

2021 iThome 鐵人賽

DAY 10
2
Modern Web

舌尖上的JS系列 第 10

D10 - 點一籠熱呼呼的小籠閉包 Closure

前言

閉包,一個完全無法從字面意思了解的專有名詞,若是改叫小籠閉包,是不是馬上聯想到這個畫面

一個個湯包被安放在蒸籠中,呼應到閉包在程式碼中的封閉區塊概念,是不是具象化許多呢

為什麼需要閉包

讓我們從以下的舉例一步步來了解為什麼要有閉包的設計

let cookie = 1;

function addCookie(){
    cookie++;
}

addCookie();
addCookie();
console.log(cookie) // 2

在全域下宣告一個值為 1 的變數 cookie及宣告一個 addCookie function,每次呼叫 cookie 值增加 1
若希望 cookie 的值 +2,呼叫兩次 addCookie,果然這時 cookie 的值增加為 3

但有個問題來了,cookie 因為存在全域變數下,有極大的風險容易被修改,若有另外的變數也叫 cookie,但起算值想從 10 開始不就撞車了?

既然變數放全域不安全,那將 cookie 宣告放到 addCookie 內試試看

function addCookie(){
    let cookie = 1;
    cookie++
    console.log(cookie);
}

addCookie(); // 2
addCookie(); // 2

稍微修改一下,變數宣告放到函式內、cookie++ 後再 return 拋出,
一樣進行兩次呼叫,結果結果的值都是 2 ?!

雖然 cookie 變數被好好的存在 addCookie 中,但每次呼叫 cookie 都重新被賦值 1,所以不管今天呼叫 addCookie 幾次得到的值都只會是 1+1 = 2 的結果

那怎麼做到每次呼叫 cookie +1 ,變數又不會被隨意修改呢?
透過 閉包 Closure 就對啦

透過閉包模擬私有變數

諸如 Java 之類的程式語言,提供了私有方法宣告的能力,意味著它們只能被同一個 class 的其他方法呼叫。

JavaScript 並沒有的提供原生的方法完成這種事,不過它藉由閉包來模擬私有方法。私有方法不只能限制程式碼的存取,它還提供了強而有力的方式來管理全域命名空間,避免非必要的方法弄亂公開介面。 - MDN

閉包的標準起手式就是之前提過的 巢狀結構 Nested function,在 function 內 return function

function addCookie(){
    let cookie = 1;
    return function (){
        cookie++
        console.log(cookie);
    } 
}

let cookieNumber = addCookie();
cookieNumber() // 呼叫匿名函式,cookie++ 變成 2
cookieNumber() // 再次呼叫匿名函式,cookie++ 變成 3

將 cookie + 1 的動作包在內層的匿名函式內,在外層將 addCookie 的呼叫結果存入變數 cookieNumber

存入 cookieNumber 的是什麼?就是閉包中的匿名函式
所以呼叫 cookieNumber 等同於執行匿名函式,得到 cookie + 1 的結果
那第二次呼叫 cookieNumber 呢? 為什麼是取得 3

記得前幾天講過的 字彙環境 和 Scope chain 嗎
匿名函式中並沒有記錄 cookie 的值,需要往外一層到 addCookie 的字彙環境查找,此時 cookie 的值因為上一次的呼叫被重新賦予值為 2,所以再加 1 的結果就是 3

那如果沒有將內部的匿名函式存入一個變數,而是用 cookieNumber( )( ) 的方式執行匿名函式呢?

function addCookie(){
    let cookie = 1;
    return function (){
        cookie++
        return cookie;
    } 
}

// 存入變數
let cookieNumber = addCookie();
cookieNumber()// 2
cookieNumber()// 3

// 使用()()
addCookie()() // 2
addCookie()() // 2

想當初又傻又天真的我以為使用兩個小括號的意思就跟存入變數後再呼叫一樣,直到被饅頭大大敲醒

如果你也跟當初的我有一樣的困惑,沒關係,加上一個 console.log 你就懂了

function addCookie(){
    let cookie = 1;
    console.log('you have to pass me first!')
    return function (){
        cookie++
        return cookie;
    } 
}

addCookie()();
// you have to pass me first!
// 2

You have to pass me first !

bytecode 內的 createClosure

記得昨天提到的使用 node 指令印出 bytecode 嗎?
輸入 node --print-bytecode --print-bytecode-filter=addCookie test.js 後印出 addCookie function 的 bytecode,發現其中一行的內容顯示為 createClosure

對應上 closure 不僅是理論上的名詞定義,在 V8 內部的實作內也確實區分出 closure 特性的操作

結語

學習過 Lexical Environment、Scope Chain 和 Execution Context 後看閉包能更快上手,一切的一切都關乎作用域的概念呀

Reference

忍者JavaScript 開發技巧探秘2 by John Resig、Bear Bibeault、Josip Maras
所有的函式都是閉包:談 JS 中的作用域與 Closure Huli 大大在詳盡的 closure 文章
MDN - 閉包
W3School
I never understood JavaScript closures
你懂 JavaScript 嗎?#15 閉包(Closure)


上一篇
D9 - 酸 V 啊酸 V8 引擎
下一篇
D11 - 分子料理 解構賦值 Destructing Assignment
系列文
舌尖上的JS30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
Chiahsuan
iT邦新手 4 級 ‧ 2021-09-25 14:37:47

/images/emoticon/emoticon12.gif
具象化的比喻(讚!)

Hooo iT邦新手 4 級 ‧ 2021-09-28 10:36:54 檢舉

一切用食物比喻就對了!

0
南國安迪
iT邦新手 3 級 ‧ 2021-09-25 14:42:00

開始討厭小籠 包了

Hooo iT邦新手 4 級 ‧ 2021-09-28 10:37:56 檢舉

鼻要討厭它

我要留言

立即登入留言