原文地址 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()
的参数。