说明: 本文来自scotch,作者:Austin Roy

文章版权属于原网站/原作者。我依旧只是个搬运工+不称职的翻译。

什么是闭包?

如果经常写 JavaScript 代码,大概率你会遇到一个既有用又经常引起困惑的概念—闭包。但什么是闭包?

闭包是函数和声明该函数的词法环境(lexical environment)的组合。

但这到底是什么意思呢?词法环境 由函数创建时的所有局部变量组成。通过闭包,可以引用函数下的所有局部变量。这实际上是通过在一个函数中定义另一个函数来实现的,在函数中的这一函数称为闭包。一旦父函数被调用,一个包含所有局部变量的全新拷贝的执行上下文将创建。在全部变量中引用局部变量可以通过与全局变量进行链接,或者是返回父函数中的闭包。

一个简单的示例将采用与此类似的:

1
2
3
4
5
function closuredFunc (){
function closure(){
// some logic
}
}

同样也可以通过以下方式让同一闭包返回多个方法:

1
2
3
4
5
6
function closure(){
function first() { console.log('I was declared first')}
function second() { console.log('I was declared second')}
function third() { console.log('I was declared third')}
return [first, second, third]
}

为了引用这些方法,我们将闭包分配给一个全局变量,然后将其指向一组公开的方法。如下所示,每个方法分配了唯一的变量名称,以将它们带入全局范围。现在,可以调用它们了。

1
2
3
4
5
6
7
8
9
let f = closure()

let one = f[0]
let two = f[1]
let three = f[2]

one() // logs I was declared first
two() // logs I was declared second
three() // logs I was declared third

为什么使用闭包?

你可能想知道为什么要花这么多时间来写闭包。然而,闭包具有许多用途与优点。

先前介绍了 ES6 中的 Class,闭包提供了类似于面向对象编程方法用于创建类似于类的隐私方法,允许我们遍历私有方法。这个也可称为 模块模式(module pattern),其允许我们更容易的维护代码,减少命名空间污染(namespace pollution)的同时增加可重用性。

让我们看一个这样做的栗子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var makeCounter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};

var counter1 = makeCounter();
var counter2 = makeCounter();

counter1.value(); // returns 0
counter1.increment(); // adds 1
counter1.increment(); // adds 1
counter1.value(); // returns 2
counter1.decrement(); //subtracts 1
counter1.value(); // returns 1
counter2.value(); // returns 0

在上面的例子中, 我们定义了一个公共函数同时也可以访问一些私有变量如 privateCounter 和其中的函数的方法 makeCounter。创建 makeCounter 模仿了类的行为,其具有内置的功能和变量。这可以在创建两个不同的计数器 counter1 和counter2 时看到。每个计数器(counter)是独立于其他计数器,指向不同状态下的变量。

闭包还允许我们使用函数来创建其他函数,这些函数会为其参数添加特定的值。在这种情况下,允许这种行为的父函数被称为 函数工厂 (function factory),因为它实际上会创建其他函数。

使用函数工厂,我们实现的这样行为称为 柯里化 (Currying)。我们将在下个部分详细描述。

柯里化

柯里化是一种函数模式用于立即执行并且返回其他函数。这使得 JavaScript 函数返回其他函数的表达式成为可能。

柯里化函数通过定义并立即返回其内部函数来链接闭包来构造。

这里有柯里化的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
let greeting = function (a) {
return function (b) {
return a + ' ' + b
}
}

let hello = greeting('Hello')
let morning = greeting('Good morning')

hello('Austin') // returns Hello Austin
hello('Roy') // returns Hello Roy
morning('Austin') // returns Good morning Austin
morning('Roy') //returns Good Morning Roy

从 greeting 创建了两个函数(hello and morning),每个返回函数处理提供的输入用来生成问候语。它们还接受了一个参数用于要打招呼的人的名字。

在上面的例子中,greeting 也作为函数工厂使用,hello 和 morning 由此生成。

在第一次调用之后,还可以继续调用内部函数,如下所示:

1
2
greeting('Hello There')('General Kenobi') 
//returns Hello There General Kenobi

柯里化认为是函数式编程的一部分,同时柯里化函数在使用 ES6 的箭头函数语法和新版本的 JavaScript 时,能更容易的书写清晰,优雅的代码。

1
2
3
4
let greeting = (a) => (b) => a + ' ' + b 

greeting('Hello There')('General Kenobi')
//returns Hello There General Kenobi

总结

自从在 ES6 有了 Class,闭包可能不会经常使用,在编写清晰的可重用代码,闭包依旧占有一席之地。在本质上与面向对象编程中的私有方法具有相似用途的函数式编程中,闭包和柯里化是其重要的概念。

参考资料

  1. Closures - JavaScript | MDN
  2. A Beginner’s Guide to Currying in Functional JavaScript/中文版