阅读 1137

[译]玩转 JavaScript 面试:何为闭包?

原文地址 Medium - Master the JavaScript Interview: What is a Closure?

坦白的讲,不掌握闭包这个知识点的话你是不会在 JavaScript 这条路上走太远的。你不仅要掌握闭包的机制是什么,还要知道闭包的重要性在哪,同时能轻松的写出几个可行的闭包用例。

闭包在 JavaScript 中经常用来进行对象数据私有化,在事件处理程序和回调函数中也常常会用到。此外还有偏分函数和柯里化及其他编程模式中也会用到闭包。

我根本不在意一个面试者是否知道「闭包」这个词或者其专业定义。我想知道的是他们是否懂得其基本的运行机制。如果面试者不知道的话,那就很明显的表示他们并没有足够的构建实际 JavaScript 应用的经验。

如果你不能回答这个问题,那你就是一个初级开发人员。我不会在乎你到底有几年的编程经验。

上面的话可能听起来很刻薄,但请你认真考虑一下我的言外之意。大多数面试官都会问你有关闭包是什么的问题,而大多数时候你的一个错误答案的代价就是失去一份工作。就算你够幸运的拿到了这份工作的 offer,你也会在年薪上无形损失上万美元。因为你会以初级开发工程师的身份被招进公司,你的工作经验有多久人家是不会在乎的。

快速小测验:你能说出两种闭包的使用场景吗?

什么是闭包?

闭包即函数与其引用的周边状态(词法环境)绑定在一起形成的(封装)组合。换句话说,闭包可以让我们从函数内部访问其外部函数的作用域。在 JavaScript 中,每当函数创建,闭包就被创建。

为了使用闭包,我们可以简单的将一个函数定义在另一个函数的内部,然后将其暴露给外部,返回这个函数或者是把它传给另一个函数。

内部函数会拥有访问外部函数作用域中变量的能力,即使是外部函数已经执行完毕并销毁。

使用闭包(示例)

闭包最常用于实现对象私有数据。数据私有是一项重要的特性,让我们能够面向接口编程而不是面向实现编程。这个重要的概念能帮助我们构建健壮的软件,因为实现细节相对于接口约定来说更容易被突发性改变。

在 JavaScript 中,闭包作为首要方式被用来实现数据私有化。当你这么做的时候,封装的变量就只能在包含(外部)函数的作用域内。你无法绕过对象被授权的方法在外部访问这些数据。在 JavaScript 中,定义在闭包作用域下的公开方法才可以访问这些数据。例如:

const getSecret = (secret) => {
  return {
    get: () => secret
  };
};

test('Closure for object privacy.', assert => {
  const msg = '.get() should have access to the closure.';
  const expected = 1;
  const obj = getSecret(1);

  const actual = obj.get();

  try {
    assert.ok(secret, 'This throws an error.');
  } catch (e) {
    assert.ok(true, `The secret var is only available
      to privileged methods.`);
  }

  assert.equal(actual, expected, msg);
  assert.end();
});
复制代码

在上例中,.get() 方法定义在 getSecret() 作用域内,这就使得它能访问 getSecret() 中的任意变量,并使其成为私有方法。在本例中它可以访问参数 secret

对象不是唯一可以产生数据私有化的东西。闭包也可以被用来创建有状态的函数,而这些函数返回的值可能会受到其内部状态的影响,例如:

const secret = msg => () => msg;
复制代码
const secret = (msg) => () => msg;

test('secret', assert => {
  const msg = 'secret() should return a function that returns the passed secret.';

  const theSecret = 'Closures are easy.';
  const mySecret = secret(theSecret);

  const actual = mySecret();
  const expected = theSecret;

  assert.equal(actual, expected, msg);
  assert.end();
});
复制代码

在函数式编程中,闭包经常被用于偏函数应用和柯里化。下面给出一些相关定义:

应用: 使用函数的参数获得返回值的过程

偏函数应用: 是传给某个函数其中一部分参数,然后返回一个新的函数,该函数等待接收后续参数的过程。换句话说,偏函数应用是一个函数,它接受另一个函数为参数,这个作为参数的函数本身接收多个参数,它返回一个函数,这个函数与它的参数相比接收更少的参数。偏函数应用提前给出一部分参数,而返回的函数则会等待调用时传入剩余的参数。

偏函数应用通过闭包作用域来提前给出参数。你可以实现一个通用的函数来给出指定的函数部分参数,示例如下:

partialApply(targetFunction: Function, ...fixedArgs: Any[]) =>
  functionWithFewerParams(...remainingArgs: Any[])
复制代码

它接收一个接收任意数量参数的函数,我们只是将部分参数应用到函数上,用返回的函数来接收剩余参数。

下面给出一个两数相加的例子:

const add = (a, b) => a + b;
复制代码

现在假设你想要一个函数,功能是对任意数字加 10,函数名为 add10()。那么 add10(5) 的结果应该是 15。因此 partialApply() 函数可以这么调用:

const add10 = partialApply(add, 10);
add10(5);
复制代码

在本例中,参数 10 作为固定参数在闭包作用域 add10() 中被记住了。

让我们来看一下 partialApply() 的一种实现:

const partialApply = (fn, ...fixedArgs) => {
  return function (...remainingArgs) {
    return fn.apply(this, fixedArgs.concat(remainingArgs));
  };
};


test('add10', assert => {
  const msg = 'partialApply() should partially apply functions'

  const add = (a, b) => a + b;

  const add10 = partialApply(add, 10);


  const actual = add10(5);
  const expected = 15;

  assert.equal(actual, expected, msg);
});
复制代码

可以看到,函数返回了一个保留了对 fixedArgs 访问的函数,而 fixedArgs 就是我们传给 partialApply() 的参数。

关注下面的标签,发现更多相似文章
评论