理解闭包

Posted by gyn on 2020-05-20

闭包是一个函数在词法域外被调用时,仍可以记住并访问它的词法域。

例如

1
2
3
4
5
6
7
8
9
function outerFun() {
var a = 2;
function innerFun() {
console.log(a);
}
return innerFun;
}
var imp = outerFun(); // Oh, I saw closure.
imp();

内部函数innerFun()的词法作用域是outerFun,执行outerFun()语句之后,我们将它的返回值(即innerFun())赋值给变量imp,此时imp指向了外部函数outerFun()的作用域,这个方式我们称之为闭包。

闭包能够让我们在定义词法作用域之外的地方,仍可以访问该作用域

闭包的使用

我们将通过回调函数、作用域、模块来理解闭包及其作用。

闭包和回调函数

即使我们没有深入理解过闭包,翻翻看我们的代码,闭包随处可见。比如写一个延时函数:

1
2
3
4
5
6
function wait(message) {
setTimeout(function timer() {
console.log(message);
}, 1000)
}
wait('Hello, closure!')

再比如,我们使用jQuery方式来实现绑定事件函数:

1
2
3
4
5
6
7
function whichButton(name, selector) {
$(selector).click(function activator() {
console.log("Activating: " + name);
});
}
whichButton("Closure Button1", "#button1");
whichButton("Closuer Button2", "#button2");

当我们传递回调函数给上述代码片段中的timer、事件绑定函数,Ajax请求、以及其他异步任务时,就是在使用闭包了。

闭包和作用域

闭包共享同一个全局变量。

1
2
3
4
5
for(var i = 0; i < 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i*1000);
}

上述代码的结果是:每隔一秒,输出一个5,共输出5次。虽然每次循环,都会复制i,但这五次循环都在共享同一个全局变量,最后一次循环i为5,故此输出5个5。如果我们想实现每个一秒,输出一次,输出结果分别是1、2、3、4、5的话:

1
2
3
4
5
for(let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i*1000);
}

let声明的迭代器变量,每次循环都会声明一次。复制代码到你的浏览器中运行下,多么神奇的块级作用域。

闭包和模块化

闭包的方式能够让我们以模块化的方式开发代码,通过提供公共API,达到模块化编程。例如,一个模块示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function CoolModule() {
var something = 'cool';
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join('!'));
}
return {
doSomething: doSomething,
doAnother: doAnother
}
}
var foo = CoolModule();
foo.doSomething();
foo.doAnother();

在JS中,这种类型的编码称为模块。模块化编程需要满足以下两个条件:

必须有一个外部函数,并且至少被调用一次(每次调用都创建一个实例)。

函数内部至少返回一个内部函数,内部函数拥有外部函数的作用域,并且可以访问和改变函数的变量。

在ES6中,引入了class语法来支持模块化编程,可以使用import的方式来引入一个或者多个模块,使用export方式来提供公共API。这种方式定义的模块内容被看作一个闭包,类似于基于闭包的函数型模块。