iT邦幫忙

2022 iThome 鐵人賽

DAY 21
0
Modern Web

致 JavaScript 開發者的 Functional Programming 新手指南系列 第 21

Day 21 :什麼是 Currying(3)?JavaScript 閉包

  • 分享至 

  • xImage
  •  

當說到閉包時,大家會有什麼樣的想法呢?是覺得它不太會在日常開發中出現,所以很少用,還是單純把閉包當成是一個面試題而已?

經過先前的章節介紹,我們了解到其實 JavaScript 的技術很多時候都是一環緊扣著另一環,如果只單純了解一個局部的概念,其實很難去理解更進階的技術要解決怎麼樣的問題,我個人認為閉包就是一個很好的代表。

如果要了解閉包是如何運作,我個人認為要有以下必備知識:

  • JavaScript 記憶體運作
  • JavaScript 函式特性
  • 執行環境、語法作用域
  • Event Loop、執行堆疊(Call Stack)

好在在先前的章節中,我們預先針對上述概念進行了了解,那究竟什麼是閉包,閉包又要解決什麼問題呢?

閉包

根據 MDN 文件,閉包的概念為:「將函式封裝進另一個函式中,如此一來被封裝的函式就可以,透過語法作用域取得到上一層函式的參考值(變數或參數)。」

咦?這不就是我們在前一章在了解執行堆疊時,所應用到的技術嗎?

沒有錯,閉包就是建立在執行堆疊與語法作用域概念的技術延伸,讓我們再看看一個範例來回憶一下這個技巧:

function makeName() {
  const name = 'Vivian';
  return function () {
    console.log(name);
  };
}
makeName()();
// Vivian

上面就是一個完整的閉包了!但上面的閉包其實離我們實際開發時,會用到的狀況差的滿多的,所以如果我們把程式碼再優化一下:

function makeName(name) {
	const printName = name;
  return function () {
    return `My name is ${name}`;
  };
};
const newName = makeName('Vivian');
console.log(newName());
//My name is  Vivian

就可以讓程式碼可用性更高了,但這邊有個小問題,我們可能會發現:咦?上方的程式碼其實根本等同於⋯⋯

const makeName = (name) => `My name is ${name}`;
const newName = makeName('Vivian');
console.log(newName);
//My name is  Vivian

根本不需要閉包,光是透過純函式就可以解決這個問題耶?但為什麼我們還需要閉包呢?

首先,使用閉包有以下好處:

  • 可以保有內部變數的獨立性,不被外部污染
  • 降低外部變數的使用,當執行堆疊結束時,記憶體及被釋放

但當然,學過純函式的我們也不一定一定需要閉包來幫我們解決上述的問題,甚至在沒有導入 FP 的狀況下,若是我們不當使用了物件作為內部變數,閉包內也依然會有 Mutation 的狀況發生。

所以,閉包不會是在 FP 中我們會使用的主要手段,在這邊我們就不針對閉包進階應用(例如:工廠模式)多加介紹,但是了解閉包對我們了解執行堆疊來說很有幫助,也是以執行堆疊運行機制為基礎更好了解、具象化的範例。

會這麼說的原因在於,也許很多人都知道閉包這個技術,但實際上卻不曉得底層的運作原理來自於函式 Event Loop 及執行堆疊的概念。

既然我們充分瞭解了執行堆疊與閉包的概念,那麼使用 FP 中進階的手法「科里化」就離我們不遠了,下一章我們將要細細解釋有關科里化的實作細節,與其要解決的問題,那我們就下一章見吧!

參考資料:

  1. MDN - Closure

上一篇
Day 20 :什麼是 Currying(2)?JavaScript Call Stack
下一篇
Day 22 :什麼是 Currying(4)?自己動手寫一個 Curry 吧!
系列文
致 JavaScript 開發者的 Functional Programming 新手指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

1
良葛格
iT邦新手 2 級 ‧ 2022-09-24 17:02:02

閉包不會是在 FP 中我們會使用的主要手段

使用純函數式語言寫程式時,幾乎隨處可見 Closure,因為太常見了,純函數式語言的介紹裡,幾乎不會提到 Closure 這個詞,因為純函數語言的 immutable 天性,Closure 捕捉的變數只能取值,沒有被變動的問題,也就不會去討論捕捉的是變數還是值這個問題。

Closure 的本意是,將當時前後文環境裡的某些東西關(close)起來,就目的而言,就是擷取當時環境裡的資訊(然後進行傳遞,這麼一來下個運算就可以基於擷取的資訊進行運算)。

純函數式語言不太提 Closure,取而代之地,常會看到的就是 high order function、function composition 之類看來高上大的名詞…XD

harry xie iT邦研究生 1 級 ‧ 2022-09-24 23:36:51 檢舉

我也想要資深前輩給予我的文章建議,感覺看很仔細

1
sixwings
iT邦研究生 3 級 ‧ 2022-09-24 20:31:55

你的第一個例子和第二個例子其實不太一樣唷
前者是**「印出」name**,後者是**「回傳」name**

第一個例子的 makeName() 建議可以使用 makeNamePrinter(),因為回傳的函式是一個單純的列印功能

第二個例子的 newName 建議可以使用 getSentence,因為 new 一般會當作形容詞或副詞,而使用動詞 get 當函式名稱開頭會讓人更容易知道這是一個函式。

然後第三個例子,如果你想要用箭頭函式改寫的話
其實應該改寫成以下寫法才是完全相同的意思

const makeName = (name) => () => `My name is ${name}`;
const newName = makeName('Vivian');
console.log(newName());

然後以第三個例子來說,並不是沒有宣告中間變數 printName 就表示沒有使用閉包
箭頭函式內的參數本身就是閉包有保存狀態的效果了

如果不相信的話,可以看下面這個例子

const XaddY = (x) => (y) => x+y
const _2addY = XaddY(2)
const _2add3 = _2addY(3) // 5

在上面的例子中,_2addY() 實際帶入的參數只有 Y(3),但它內部仍然有保存 X(2) 的狀態
所以閉包的範圍不僅限於大括號裡面的變數,還有包含函式呼叫時的參數列

如果覺得上面那個例子太複雜看不懂的話,你可以把第二個例子中的 const printName = name; 移除。你會發現第二個例子一樣可以正常運作,這證明閉包對參數列帶入的參數一樣適用

以上,路過看到小小補充一下

我要留言

立即登入留言