閉包是 JavaScript 裡的一種程式設計模式,它允許外層語彙作用域裡的變數在巢狀的程式區塊內使用。 JavaScript 對閉包的支援是透明的,你常常在不知不覺中就用到了它們,甚至不知道它們是什麼。
// Top-level declarations are global-scope
const dozen = 12;
{
// Braces create a new block-scope
// Referencing the outer variable is a closure.
const twoDozen = dozen * 2;
}
// Because of the block-scope declaration, twoDozen is not available here.
twoDozen;
// => Uncaught ReferenceError: twoDozen is not defined
除了大括號 {} 之外,函式(以及類別)也會建立新的作用域,並且可以_包住_值:
const dozen = 12;
// Functions create a new function-scope and block-scope.
// Referencing the outer variable here is a closure.
function nDozen(n) {
// This is declared inside the function scope, and uses the top-level scope.
// This works, and encloses the value 12.
const twoDozen = dozen * 2;
// This only uses the locally declared variable and the passed argument to the parameter `n`
return (twoDozen / 2) * n;
}
// Because of the function-scope declaration, twoDozen is not available here.
twoDozen;
// => Uncaught ReferenceError: twoDozen is not defined
從twoDozen的例子可以看出,值可以被包在一個_巢狀_作用域(函式、區塊等)裡,但沒辦法從那個情境中被拉出來。
在大多數情況下,現代 JavaScript 的設計就是要讓值不會_外洩_到外層作用域。
使用可變的變數宣告(例如let或var)可以保存一些狀態:
let counter = 0;
// This function closure increments the counter's state in the outer lexical context.
// This way the counter can be shared between many calling contexts.
export function increment() {
counter += 1;
return counter;
}
increment();
// => 1
counter;
// => 1
把這兩個想法結合起來:包住值來保存狀態,以及被包住的值不會外洩到外面,就能建立私有值。
最常見的方法是建立一個會回傳函式的函式,而這個回傳的函式包住了某些狀態。
export function makeCounter() {
let counter = 0;
// This returns a new function that encloses the local variable counter
return function increment() {
counter += 1;
return counter;
};
}
// Counter did not leak
counter;
// => Uncaught ReferenceError: counter is not defined
// This creates a new counter.
// This assigns the increment function to the variable myFirstCounter.
const myFirstCounter = makeCounter();
typeof myFirstCounter;
// => function
myFirstCounter.name;
// => increment
myFirstCounter();
// => 1
myFirstCounter();
// => 2
// This creates new counter (with new, separate local state / enclosed counter variable)
const mySecondCounter = makeCounter();
mySecondCounter();
// => 1
// It does not affect the first counter.
myFirstCounter();
// => 3
很多程式設計師覺得閉包是個很難的概念,而且從函式回傳函式在某些程式語言裡並不常見,甚至根本做不到。 如果你想找更多閱讀材料,MDN 上的閉包指南相當完整。
你的設計公司一直主要使用 CSS 轉換來打造網頁。經過一番討論後,決定開始使用 JavaScript 來動態執行一些計算。由於你的一些隊友對 JavaScript 比較不熟悉,所以你決定使用函式閉包,為{x, y}座標對建立可重複使用的轉換。
實作translate2d函式,它會回傳一個運用閉包的函式,用來對一組座標對進行可重複的 2D 平移。
在幾何學中,平移指的是將點、向量或圖形朝同一個方向移動相同的距離。 也可以理解為對每個點加上一個常數。
const moveCoordinatesRight2Px = translate2d(2, 0);
const result = moveCoordinatesRight2Px(4, 8);
// result => [6, 8]
實作scale2d函式,它會回傳一個運用閉包的函式,用來對一組座標對進行可重複的 2D 縮放。
在幾何學中,均勻的縮放指的是將向量或圖形朝同一個方向放大或縮小。 也可以理解為將每個點乘上一個常數(縮放係數)。
在這個練習中,假設縮放值都是正數。
const doubleScale = scale2d(2, 2);
const result = doubleScale(6, -3);
// result => [12, -6]
組合兩個轉換函式,以進行可重複的轉換。這通常稱為_函式合成_,其中第一個函式_'f(x)'的結果會作為第二個函式'g(x)'_的輸入。
const moveCoordinatesRight2Px = translate2d(2, 0);
const doubleCoordinates = scale2d(2, 2);
const composedTransformations = composeTransform(
moveCoordinatesRight2Px,
doubleCoordinates,
);
const result = composedTransformations(0, 1);
// result => [4, 2]
實作memoizeTransform函式。它會接收一個要_記憶化_的函式,然後回傳一個新函式,這個新函式會記住傳入函式的輸入,這樣最後一個回傳值就能被「記住」,並且在再次以相同引數呼叫時只計算一次。
記憶化有時會用在_動態規劃_中。 它讓耗費資源的運算只需執行一次,因為結果會被記住。 注意,在這個練習中只會記住最後一個結果,這點和動態規劃中會記憶_所有_結果的部分解法不同。
const tripleScale = scale2d(3, 3);
const memoizedScale = memoizeTransform(tripleScale);
memoizedScale(4, 3); // => [12, 9], this is computed since it hasn't been computed before for the arguments
memoizedScale(4, 3); // => [12, 9], this is remembered, since it was computed already