轨道
/
JavaScript
JavaScript
/
练习
/
坐标变换
坐标变换

坐标变换

学习练习

简介

闭包是 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函数,它返回一个函数,这个函数利用闭包对坐标对执行可重复的二维平移。

在几何中,平移指的是把点、向量或图形沿同一方向移动相同的距离,也可以理解为给每个点加上一个常数。

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

2. 缩放坐标

实现scale2d函数,它返回一个函数,这个函数利用闭包对坐标对执行可重复的二维缩放。

在几何中,均匀缩放指的是把向量或图形沿同一方向放大或缩小,也可以理解为把每个点乘以一个常数(缩放系数)。

本题假定缩放值都是正数。

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,全部免费。