當說到閉包時,大家會有什麼樣的想法呢?是覺得它不太會在日常開發中出現,所以很少用,還是單純把閉包當成是一個面試題而已?
經過先前的章節介紹,我們了解到其實 JavaScript 的技術很多時候都是一環緊扣著另一環,如果只單純了解一個局部的概念,其實很難去理解更進階的技術要解決怎麼樣的問題,我個人認為閉包就是一個很好的代表。
如果要了解閉包是如何運作,我個人認為要有以下必備知識:
好在在先前的章節中,我們預先針對上述概念進行了了解,那究竟什麼是閉包,閉包又要解決什麼問題呢?
根據 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 中進階的手法「科里化」就離我們不遠了,下一章我們將要細細解釋有關科里化的實作細節,與其要解決的問題,那我們就下一章見吧!
閉包不會是在 FP 中我們會使用的主要手段
使用純函數式語言寫程式時,幾乎隨處可見 Closure,因為太常見了,純函數式語言的介紹裡,幾乎不會提到 Closure 這個詞,因為純函數語言的 immutable 天性,Closure 捕捉的變數只能取值,沒有被變動的問題,也就不會去討論捕捉的是變數還是值這個問題。
Closure 的本意是,將當時前後文環境裡的某些東西關(close)起來,就目的而言,就是擷取當時環境裡的資訊(然後進行傳遞,這麼一來下個運算就可以基於擷取的資訊進行運算)。
純函數式語言不太提 Closure,取而代之地,常會看到的就是 high order function、function composition 之類看來高上大的名詞…XD
我也想要資深前輩給予我的文章建議,感覺看很仔細
你的第一個例子和第二個例子其實不太一樣唷
前者是**「印出」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;
移除。你會發現第二個例子一樣可以正常運作,這證明閉包對參數列帶入的參數一樣適用
以上,路過看到小小補充一下