You Don't Know JS上卷Part1 Chapter5.作用域闭包

5.1 闭包

  • 当函数可以记住并访问所在的词法作用域,即使函数名是在当前词法作用域之外执行,这时就产生了闭包。
1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
var a = 2;

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

return bar;
}

var baz = foo();

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

bar()在自己定义的词法作用域以外的地方执行。

bar()拥有覆盖foo()内部作用域的闭包,使得该作用域能够一直存活,以供bar()在之后任何时间进行引用,不会被垃圾回收器回收

  • bar()持有对foo()内部作用域的引用,这个引用就叫做闭包。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 对函数类型的值进行传递
function foo() {
var a = 2;

function baz() {
console.log( a ); // 2
}

bar( baz );
}

function bar(fn) {
fn(); // 这就是闭包
}

foo();
  • 把内部函数baz传递给bar,当调用这个内部函数时(现在叫做fn),它覆盖的foo()内部作用域的闭包就形成了,因为它能够访问a。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 间接的传递函数
var fn;

function foo() {
var a = 2;

function baz() {
console.log( a );
}

fn = baz; // 将baz分配给全局变量
}

function bar() {
fn(); // 这就是闭包
}

foo();
bar(); // 2
  • 将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
1
2
3
4
5
6
7
8
function wait(message) {

setTimeout( function timer() {
console.log( message );
}, 1000 );
}

wait( "Hello, closure!" );
  • 在引擎内部,内置的工具函数setTimeout(..)持有对一个参数的引用,这里参数叫做timer,引擎会调用这个函数,而词法作用域在这个过程中保持完整。这就是闭包
  • 定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包
1
2
3
4
5
6
// 典型的闭包例子:IIFE
var a = 2;

(function IIFE() {
console.log( a );
})();

5.2 循环和闭包

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

//输入五次6
  • 延迟函数的回调会在循环结束时才执行,输出显示的是循环结束时i的最终值。
  • 尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i

尝试方案1:使用IIFE增加更多的闭包作用域

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

//失败,因为IIFE作用域是空的,需要包含一点实质内容才可以使用

尝试方案2:IIFE增加变量

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

// 正常工作

尝试方案3:改进型,将i作为参数传递给IIFE函数

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

// 正常工作
5.2.1 块作用域和闭包
  • let可以用来劫持块作用域,并且在这个块作用域中声明一个变量。
  • 本质上这是将一个块转换成一个可以被关闭的作用域
1
2
3
4
5
6
7
8
for (var i = 1; i <= 5; i++) {
let j = i; // 闭包的块作用域!
setTimeout( function timer() {
console.log( j );
}, j * 1000 );
}

// 正常工作
  • for循环头部的let声明会有一个特殊的行为。变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。

上面这句话参照3.4.3–—2.let循环,即以下

1
2
3
4
5
6
7
{
let j;
for (j = 0; j < 10; j++) {
let i = j; // 每个迭代重新绑定!
console.log( i );
}
}

循环改进:

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

// 正常工作

5.3 模块

模块模式需要具备两个必要条件:

  • 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例,可以通过IIFE实现单例模式)
  • 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
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
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(); // cool
foo.doAnother(); // 1 ! 2 ! 3

// 1、必须通过调用CoolModule()来创建一个模块实例
// 2、CoolModule()返回一个对象字面量语法{ key: value, ... }表示的对象,对象中含有对内部函数而不是内部数据变量的引用。内部数据变量保持隐藏且私有的状态。
  • 使用IIFE实现单例模式

立即调用这个函数并将返回值直接赋予给单例的模块标识符foo。

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

foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

5.5.1 现代的模块机制

大多数模块依赖加载器/管理器本质上是将这种模块定义封装进一个友好的API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 ); // 核心,为了模块的定义引用了包装函数(可以传入任何依赖),并且将返回值(模块的API),储存在一个根据名字来管理的模块列表中。
}

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

return {
define: define,
get: get
};

})();

使用上面的函数来定义模块:

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
MyModules.define( "bar", [], function() {
function hello(who) {
return "Let me introduct: " + who;
}

return {
hello: hello
};
} );

MyModules.define( "foo", ["bar"], function(bar) {
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" );
) // Let me introduct: hippo

foo.awesome(); // LET ME INTRODUCT: HIPPO

5.5.2 未来的模块机制

在通过模块系统进行加载时,ES6会将文件当做独立的模块来处理。每个模块都可以导入其他模块或特定的API成员,同样可以导出自己的API成员。

ES6模块没有“行内”格式,必须被定义在独立的文件中(一个文件一个模块)

  • 基于函数的模块不能被静态识别(编译器无法识别),只有在运行时才会考虑API语义,因此可以在运行时修改一个模块的API。
  • ES6模块API是静态的(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
// bar.js

function hello(who) {
return "Let me introduct: " + 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“模块
module foo from "foo";
module bar from "bar";

console.log(
bar.hello( "rhino")
); // Let me introduct: rhino

foo.awesome(); // LET ME INTRODUCT: HIPPO
  • import:将一个模块中的一个或多个API导入到当前作用域中,并分别绑定在一个变量上
  • module:将整个模块的API导入并绑定到一个变量上。
  • export:将当前模块的一个标识符(变量、函数)导出为公共API