作用域闭包

魔术师的幕后藏着一个人,我们将要揭开他的伪装

启示

JavaScript中闭包无处不在,你只需要能够识别并拥抱它。

闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意 识地创建闭包。闭包的创建和使用在你的代码中随处可见。你缺少的是根据你自己的意愿 来识别、拥抱和影响闭包的思维环境。

实质问题

一个闭包的直接定义,当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使喊你数是在当前词法作用域之外执行

以下的例子是闭包吗?

1
2
3
4
5
6
7
8
9
10
11
function foo () {
var a = 2

function bar () {
console.log(a) //2
}

bar()
}

foo()

这是闭包吗?
技术上来讲,也许是。但根据前面的定义,确切地说并不是。我认为最准确地用来解释 bar() 对 a 的引用的方法是词法作用域的查找规则,而这些规则只是闭包的一部分。(但却 是非常重要的一部分!)

以下代码清晰展示了闭包:

1
2
3
4
5
6
7
8
9
10
11
12
function foo () {
var a = 2

function bar () {
console.log(a)
}
return bar
}

bar baz = foo()

baz() //2 ,这就是闭包的效果

函数bar()的词法作用域能够访问foo()的内部作用域,然后将bar()函数本身当做一个值类型来进行传递。
在这个例子中,bar()函数在自己定义的词法作用域以外的地方执行。

闭包阻止了foo()的内存回收

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

1
2
3
4
5
6
7
8
9
10
11
12
13
(function foo() {
var a = 2

function bar() {
console.log(a)
}

barz(bar)
})() //立即执行

function barz(fn) {
fn()
}

把内部函数 baz 传递给 bar,当调用这个内部函数时(现在叫作 fn),它涵盖的 foo() 内部
作用域的闭包就可以观察到了,因为它能够访问 a。

也可以是间接的传递函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
var fn
function foo() {
var a = 2
function baz() {
console.log(a)
}
fn = baz //将baz分配给全局变量
}
function bar() {
fn() //闭包
}
foo()
bar()

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

闭包小结

本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一 级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、 Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使 用了回调函数,实际上就是在使用闭包!

循环和闭包

输出结果为5次6,延迟函数会在循环结束时才执行,即使将延时时间设为0,也会等待循环结束之后才执行。它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。

以下方法是否可行?

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

依然是输出5次6,这个IIFE只是一个什么都没有的空作用域,需要包含一点实质性内容才能为我们所用,它需要有自己的变量

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

ok,再对IIFE进行改进!

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

在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的
作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

重返块作用域

仔细思考我们对前面的解决方案,我们使用IIFe在每次迭代时都创建了一个新的作用域,换句话说,每次迭代我们都需要一个块作用域。 使用let,可以用来劫持块作用域,并且在这个块作用域中声明一个变量。

本质上这是将一个块转换成一个可以被关闭的作用域

因此,上面的循环闭包可以写成:

1
2
3
4
5
for (var i=1; i<=5; i++) {
let j = i; // 是的,闭包的块作用域! setTimeout( function timer() {
console.log( j );
}, j*1000 );
}

for 循环头部的 let 声明还会有一 个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随 后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。因此可以改写成:

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

模块

一个模块的例子:

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
function CoolModult() {
var something = 'cool'
var anoter = [1,2,3]

function doSomething() {
console.log(something)
}

function doAnohter() {
console.log(anoter.join(' ! '))
}

return {
doSomething: doSomething,
doAnohter: doAnohter
}
}

var foo = CoolModult()

foo.doSomething()
foo.doAnohter()

/*
cool
1 ! 2 ! 3
*/

首先,CoolModule() 只是一个函数,必须要通过调用它来创建一个模块实例。如果不执行 外部函数,内部作用域和闭包都无法被创建。

其次,CoolModule() 返回一个用对象字面量语法 { key: value, … } 来表示的对象。

引申出来的,模块模式需要具备两个必要条件:

  • 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)
  • 封闭函数必须返回至少一个内部函数,这样的内部函数才能在私有作用于中形成闭包,并且可以访问或者修改私有状态

改写为单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var foo = (function CoolModult() {
var something = 'cool'
var anoter = [1,2,3]

function doSomething() {
console.log(something)
}

function doAnohter() {
console.log(anoter.join(' ! '))
}

return {
doSomething: doSomething,
doAnohter: doAnohter
}
})()


foo.doSomething()
foo.doAnohter()

将模块函数转换成了 IIFE(参见第 3 章),立即调用这个函数并将返回值直接赋值给 单例的模块实例标识符 foo。

现代的模块机制

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
51
52
53
54
55
var MyModules = (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
}
})()

MyModules.define('bar', [], function() {
function hello(who) {
return 'Let me introduce: ' + who
}

return {
hello: hello
}
})

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

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

return {
awesome: awesome
}
})

var bar = MyModules.get('bar')
var foo = MyModules.get('foo')

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

foo.awesome()

/**
* Let me introduce: hippo
* LET ME INTRODUCE: HIPPO
*/

这段代码的核心是modules[name] = impl.apply(impl, deps)。为了模块的定义引入了包装 函数(可以传入任何依赖),并且将返回值,也就是模块的 API,储存在一个根据名字来管 理的模块列表中。

未来的模块机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bar.js
function hello(who) {
return "Let me introduce: " + who;
}
export hello; foo.js
// 仅从 "bar" 模块导入 hello() import hello from "bar";
var hungry = "hippo";
function awesome() { console.log(
hello( hungry ).toUpperCase()
);
}
export awesome;
baz.js
// 导入完整的 "foo" 和 "bar" 模块 56 | 第 5 章

module foo from "foo";
module bar from "bar";
console.log(
bar.hello( "rhino" )
); // Let me introduce: rhino
foo.awesome(); // LET ME INTRODUCE: HIPPO

import 可以将一个模块中的一个或多个 API 导入到当前作用域中,并分别绑定在一个变量 上(在我们的例子里是 hello)。module 会将整个模块的 API 导入并绑定到一个变量上(在 我们的例子里是 foo 和 bar)。export 会将当前模块的一个标识符(变量、函数)导出为公 共 API。这些操作可以在模块定义中根据需要使用任意多次。

模块文件中的内容会被当作好像包含在作用域闭包中一样来处理,就和前面介绍的函数闭 包模块一样。

小结

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时 就产生了闭包。

模块有两个主要特征:

  • 为创建内部作用域而调用了一个包装函数;
  • 包装函数的返回 值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭 包。