一、前言
闭包是基于词法作用域( 和动态作用域对应,词法作用域是由你写代码时,将变量写在哪里来决定的,因此当词法分析器处理代码时,会保持作用)书写代码时所产生的自然结果,甚至不需要为了利用闭包而有意地创建闭包。闭包的创建和使用在动态语言的代码中随处可见。你缺少的只是识别,拥抱和使用闭包的思维。
当函数可以记住并访问所在的词法作用域,即使函数在当前词法作用域之外执行。就产生了闭包。
一般情况下,当函数执行完毕,垃圾回收机制会期待函数的整个内部作用域被销毁,但当闭包存在时,会阻止这件事情的发生,事实上内部作用域依旧存在,此时内部函数依旧持有对外部函数作用域的引用,这个引用就叫做闭包。无论通过何种方式将内部函数传递到所在的词法作用域之外,他都会持有对 原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
所以说,在javascript,python这种动态语言中,因为函数是一级对象,无论何时何地,只要将函数当做第一级的值类型并到处传递,都会看到闭包的运用,可以说,闭包无处不在。
二、循环与闭包
要说明闭包,for循环是最常见的例子。
1 | for(var i = 1; i < 5; i++) { |
正常情况下,我们对这段代码行为的预期分别是输出数字1~5,每秒一次,每次一个。
但实际上,这段代码在运行时会以每秒一次的频率输出五次5。
这里引申出一个更深入的问题,代码中到底有什么缺陷导致他的行为同语义所暗示的不一致呢?
缺陷是我们试图假设循环中的每个迭代在运行时,都会给自己“捕获一个i的副本”。但是根据作用域的工作原理,实际情况是尽管循环中的5个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中
,因此实际上只有一个i。
下面回到正题,缺陷是什么?我们需要更多的闭包作用于,特别是在循环的过程中每个迭代都需要一个闭包作用域。
IIFE会通过声明并立即执行一个函数来创建作用域。
1 | for(var i = 1; i < 5; i++) { |
这样可以吗?
不行。
如果作用域是空的,那么仅仅将他们封闭起来是不够的。仔细看一下,我们的IIFE只是一个什么都没有的空作用域,他需要包含一点实质内容才能为我们所用。
他需要有自己的变量,用来在每个迭代中存储i的值:
1 | for(var i = 1; i < 5; i++) { |
ES6的let声明
let可以用来劫持块作用域,并且在这个块作用域中声明一个变量。
本质上这是一个将一个块转换成一个可以被关闭的作用域。然后下面这些看起来很酷的代码就可以正常运行了:1
2
3
4
5
6
7
8
9
10
11
12for(var i = 1; i < 5; i++) {
let j = i
setTimeout(() => {
console.log(j)
}, j * 1000)
}
for(let i = 1; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, i * 1000)
}
三、模块
模块是一个利用闭包的典型案例:
模块模式至少具备两个条件:
1)必须有外部的封闭包装函数来创建内部作用域,该函数至少被调用一次(每次调用都会创建一个新的模块作用域),如果是ITFE调用就只产生一个实例(单例模式);
2)封闭函数返回至少一个内部函数的引用(可以直接返回该内部函数,如jQuery;也可以返回一个对象,该对象至少包含一个属性,指向内部函数的引用),这样内部函数才能在私有作用域形成闭包,而且可以访问或者修改私有的状态;
比如模块的一个很常见的应用就是返回作为公共API返回的对象:
1 | var foo = (function () { |
模块模式另一个简单但强大的用法是命名将要作为公共API返回的对象:
1 | var foo = function () { |
通过在模块实例的内部保留对公共API对象的内部引用,可以从内部对模块实例进行修改,包括添加删除方法和属性,以及修改他们的值。
四、模块加载器/管理器
模块管理器本质上并没有任何的“魔力”,本质上就是讲模块定义封装进一个友好的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
41var MyModules = (function () {
var modules = []
//参数name:模块名称
//参数deps:依赖的模块名称
//impl:名为name的模块实现
var define = function define (name, deps, impl) {
var i = 0;
for(i; i<deps.length; i++){
deps[i]=modules[deps[i]];
}
modules[name] = impl.apply(impl,deps);
};
var get = function (name) {
return modules[name];
};
return {
define:define,
get:get
}
})();
//调用
MyModules.define('bar',[],function(){
function hello(){
return 'hello';
}
return {
hello:hello,
};
});
MyModules.define('foo',['bar'],function(bar){
function awesome(){
console.log('foo ' + bar.hello());
}
return{
awesome:awesome,
}
})
var bar=MyModules.get('bar');
var foo=MyModules.get('foo');
foo.awesome();
foo
和bar
模块东营市通过一个返回公共API的函数来定义的,foo
甚至接受bar
的实例作为以来参数,并能相应的使用它。
ES6的模块机制
ES6中为模块增加了一级语法支持。在通过模块系统进行加载时,ES6会将文件当做独立的模块来处理。每个模块都可以导入其他模块或特定的API成员,同样也可以导出自己的API成员。
ES6的模块没有“行内”格式,必须被定义在独立的文件中(一个文件一个模块)。浏览器或引擎有一个默认的“模块加载器”可以在导入模块是同步的加载模块文件。
1 | bar.js |
import可以将一个模块中的一个或多个API导入到当前作用域中,并分别绑定在一个变量上。expor会将模块的一个标识符(变量,函数)导出为公共API。
模块文件中的内容会被当做好像包含在作用域闭包中一样来处理,就和前面介绍的函数闭包模块一样。