0%

js闭包

JavaScript中闭包无处不在,我们只需要去识别并接受它。当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

闭包是什么样的?

可以来看一段代码,清晰的展示闭包:

1
2
3
4
5
6
7
8
9
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2,闭包的效果

函数bar()的词法作用域能够访问foo()的内部作用域,然后我们将函数bar()本身作为一个值类型进行传递,将bar所引用的函数对象本身当做返回值。

foo()执行后,其返回值(也就是内部的bar()函数)赋值给变量baz,并调用baz(),实际上时通过不同的标识符引用调用了内部的函数bar()

bar()虽然可以正常运行,但是它在定义自己的词法作用域以外的地方执行。

foo()执行后,通常会期待foo()的整个内部作用域都被销毁,由于看上去foo()的内容不会再被使用,所以很自然地会考虑对其进行回收。然而闭包神奇支出正是可以阻止这件事情的发生。事实上内部作用域依旧存在,因此并没有被回收。可以发现,是bar()本身在使用这个内部作用域。

由于bar()声明于foo()的内部,所以bar()拥有foo()内部作用域的闭包,使得该作用域能够一直存活,以供bar()在之后任何时间进行引用。bar()依然持有对该作用域的引用,而这个引用就叫做“闭包”。

因此,在baz变量被实际调用的时候,它访问到了定义bar()时的词法作用域,因此它可以如预期般访问变量a。即,闭包使函数可以继续访问定义时的词法作用域。

当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时,都可以观察到闭包。

无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义域的引用,无论在何处执行这个函数都会使用闭包。

我们来看一个常用的延时函数:

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

将一个内部函数timer()传递给setTimeout()timer具有涵盖wait()作用域的闭包,因此还保有对变量message的引用。

深入分析下,内置的工具函数setTimeout()持有对一个参数的引用,引擎会调用传递过去的这个函数,而词法作用域在这个过程中保持完整,这就是闭包

for循环和闭包

再看for循环。我们前面看了let这个关键字可以来劫持块作用域,并且在这个块作用域中声明一个变量。这样我们可以用let和for结合来实现一些酷酷的东西。let声明的变量不止被声明一次,每次迭代都会声明,随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。

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

这样我们可以就正确的延时输出1,2,3,4,5,6了。

模块

有很多的代码模式利用闭包的强大威力,但从表面上看似乎与回调无关,模块就是其中一种。

先来看一个简单的模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function FooModule() {
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 = FooModule();
foo.doSomething();

这种模式在JavaScript中被称为模块。最常见的实现模块模式的方法通常被称为模块暴露。

我们来分析一下上面这个FooModule模块。

  • 首先,它是一个函数,必须要调用它才能创建一个模块实例,否则内部作用域和闭包都无法创建。
  • 其次,它返回一个用对象字面量语法{key: value}来表示的对象。这个返回的对象中含有对内部函数的引用,这样可以保持内部数据变量是隐藏且私有状态。
  • 这个对象最终被赋值给外部的变量foo,然后就可以通过foo来访问模块中的属性方法。

返回一个实际的对象不是必须的,也可以直接返回一个内部的函数。

简单来讲,模块模式需要具备两个必须条件:

  • 必须有外部的封闭函数,该函数必须至少被调用一次。
  • 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有状态。

现代的模块机制

大多数模块加载器/管理器本质上都是将这种模块定义分装进一个友好的API,例如简单的一个模块依赖实现:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
var MyModles = (function Manager() {
var modules = {};

function define(name, deps, impl) {
for (var i = 0; i < deps.length; i ++) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply(impl, deps);
}

function get(name) {
return modules[name];
}

return {
define: define,
get: get,
}
})();

MyModles.define('bar', [], function(){
function hello(who) {
return "let me introduce: " + who;
}

return {
hello: hello,
}
});

MyModles.define('foo', ["bar"], function(bar){
var hungry = 'hippo';

console.log(bar);

function awesome() {
console.log(bar.hello(hungry).toUpperCase());
}

return {
awesome: awesome,
}
});

var bar = MyModles.get('bar');
var foo = MyModles.get('foo');

console.log(bar.hello('hippo'));

foo.awesome();

未来的模块机制

ES6中为模块增加了一级语法支持。当通过模块系统进行加载时,ES6会将文件作为独立的模块来处理。每个模块都可以导入其他模块或特定的API成员,同样也可以导出自己的API成员。使用关键字exportimport来导出和导入一个模块。

码字辛苦,打赏个咖啡☕️可好?💘