座標轉換

座標轉換

學習練習

簡介

閉包是 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
Note

很多程式設計師覺得閉包是個很難的概念,而且從函式回傳函式在某些程式語言裡並不常見,甚至根本做不到。 如果你想找更多閱讀材料,MDN 上的閉包指南相當完整。

說明

你的設計公司一直主要使用 CSS 轉換來打造網頁。經過一番討論後,決定開始使用 JavaScript 來動態執行一些計算。由於你的一些隊友對 JavaScript 比較不熟悉,所以你決定使用函式閉包,為{x, y}座標對建立可重複使用的轉換。

1. 平移座標

實作translate2d函式,它會回傳一個運用閉包的函式,用來對一組座標對進行可重複的 2D 平移。

在幾何學中,平移指的是將點、向量或圖形朝同一個方向移動相同的距離。 也可以理解為對每個點加上一個常數。

const moveCoordinatesRight2Px = translate2d(2, 0);
const result = moveCoordinatesRight2Px(4, 8);
// result => [6, 8]

2. 縮放座標

實作scale2d函式,它會回傳一個運用閉包的函式,用來對一組座標對進行可重複的 2D 縮放。

在幾何學中,均勻的縮放指的是將向量或圖形朝同一個方向放大或縮小。 也可以理解為將每個點乘上一個常數(縮放係數)。

在這個練習中,假設縮放值都是正數。

const doubleScale = scale2d(2, 2);
const result = doubleScale(6, -3);
// result => [12, -6]

3. 組合轉換函式

組合兩個轉換函式,以進行可重複的轉換。這通常稱為_函式合成_,其中第一個函式_'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]

4. 儲存函式的結果

實作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
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
JavaScript Exercism

準備好開始 座標轉換 了嗎?

註冊 Exercism,透過 37 個概念159 個練習 和真人引導來學習並精通 JavaScript,全部免費。