JavaScript 解密 —— 函数进阶(闭包与生成器)

一、闭包

简单来说,闭包(closure)允许函数访问和操作位于自身外部的变量。
借助闭包的特性,函数可以访问任何变量及其他函数,只要这些数据在该函数定义时位于其作用域内部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var outerValue = "samurai"
var later

function outerFunction() {
var innerValue = "ninja"

function innerFunction() {
console.log(outerValue)
console.log(innerValue)
}
later = innerFunction
}

outerFunction()
later()
// samurai
// ninja

参考上面的代码,按照通常的理解:

  • 变量 outerValue 定义在全局作用域中,因此其可以从程序的任意位置访问
  • outerFunction 执行,将 innerFunction 关联给全局变量 later
  • laterinnerFunction)执行时,outerFunction 已经执行完毕,其内部的作用域理应失效,无法被 later 访问
  • innerValue 由于在 outerFunction 内部定义,则 later 访问 innerValue 时其值应该为 undefined

实际上程序输出的 innerValue 的值为 ninja,即 outerFunction 内部定义的 innerValue 可以被 later 访问。这就是闭包所产生的效果。

当我们在 outerFunction 内部声明 innerFunction 时,一个包含当前作用域(“当前”指的是内部函数定义的时刻)中所有变量的闭包同时被创建。最终 innerFunction 执行时,即便其声明时的原始作用域已经消失,innerFunction 还是可以通过闭包访问其原始作用域。
闭包像是使用了一个“保护层”将函数定义时的作用域封闭起来,只要该函数的生命周期未结束,“保护层”内的作用域就一直可以被访问。

二、闭包的现实应用

模拟私有变量

私有变量即从对象外部不可见的变量,可以向用户隐藏对象内部不必要的实现细节。
JavaScript 没有对私有变量的原生支持,但是通过闭包可以实现类似的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Ninja() {
var feints = 0
this.getFeints = function() {
return feints
}
this.feint = function() {
feints++
}
}

var ninja1 = new Ninja()
ninja1.feint()

console.log(ninja1.feints) // undefined
console.log(ninja1.getFeints()) // 1

var ninja2 = new Ninja()
console.log(ninja2.getFeints()) // 0

在回调函数中使用闭包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<button id="box1">First Button</button>
<script>
function animateIt(elementId) {
var elem = document.getElementById(elementId)
var tick = 100
var timer = setInterval(function() {
if (tick < 1000) {
elem.style.width = tick + "px"
tick += 10
} else {
clearInterval(timer)
}
}, 100)
}
animateIt("box1")
</script>

在上面的代码中,一个匿名函数作为参数(回调函数)传递给 setInterval,令指定元素的宽度能够随时间增长以形成动画效果。该匿名函数借助闭包能够访问外部定义的 elemticktimer 三个参数,控制动画的进度。
这三个参数定义在 animateIt 内部通过闭包被回调函数访问,而不是直接在全局作用域中定义。这样可以避免多个 animateIt 函数依次运行时引起冲突。

三、生成器

生成器是一种可以生成一系列值的特殊函数,只不过这些值不是同时产生的,需要用户显式地去请求新值(通过 fornext 等)。

1
2
3
4
5
6
7
8
9
10
11
12
13
function* WeaponGenerator() {
yield "Katana"
yield "Wakizashi"
yield "Kusarigama"
}

for(let weapon of WeaponGenerator()) {
console.log(weapon)
}

// Katana
// Wakizashi
// Kusarigama

调用生成器并不意味着会逐步执行生成器函数的定义代码,而是会创建一个迭代器(iterator)对象,通过这个迭代器对象与生成器进行交互(如请求新的值)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function* WeaponGenerator() {
yield "Katana"
yield "Wakizashi"
}

const weaponsIterator = WeaponGenerator()

const result1 = weaponsIterator.next()
console.log(typeof result1, result1.value, result1.done)
// object Katana false

const result2 = weaponsIterator.next()
console.log(typeof result2, result2.value, result2.done)
// object Wakizashi false

const result3 = weaponsIterator.next()
console.log(typeof result3, result3.value, result3.done)
// object undefined true

使用 while 遍历生成器:

1
2
3
4
5
6
7
8
9
10
11
12
13
function* WeaponGenerator() {
yield "Katana"
yield "Wakizashi"
}

const weaponsIterator = WeaponGenerator()
let item
while(!(item = weaponsIterator.next()).done) {
console.log(item.value)
}

// Katana
// Wakizashi

生成器嵌套:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function* WarriorGenerator() {
yield "Sun Tzu"
yield* NinjaGenerator()
yield "Genghis Khan"
}

function* NinjaGenerator() {
yield "Hattori"
yield "Yoshi"
}

for(let warrior of WarriorGenerator()) {
console.log(warrior)
}

// Sun Tzu
// Hattori
// Yoshi
// Genghis Khan

生成器的应用

生成 ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* IdGenerator() {
let id = 0
while (true) {
yield ++id
}
}

const idIterator = IdGenerator()
const ninja1 = { id: idIterator.next().value }
const ninja2 = { id: idIterator.next().value }
const ninja3 = { id: idIterator.next().value }

console.log(ninja1.id) // 1
console.log(ninja2.id) // 2
console.log(ninja3.id) // 3

遍历DOM

使用递归函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div id="subTree">
<form>
<input type="text" />
</form>
<p>Paragraph</p>
<span>Span</span>
</div>
<script>
function traverseDOM(element, callback) {
callback(element)
element = element.firstElementChild
while (element) {
traverseDOM(element, callback)
element = element.nextElementSibling
}
}
const subTree = document.getElementById("subTree")
traverseDOM(subTree, function(element) {
console.log(element.nodeName)
})
</script>

使用生成器(无需借助 callback):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div id="subTree">
<form>
<input type="text" />
</form>
<p>Paragraph</p>
<span>Span</span>
</div>
<script>
function* DomTraversal(element) {
yield element
element = element.firstElementChild
while (element) {
yield* DomTraversal(element)
element = element.nextElementSibling
}
}

const subTree = document.getElementById("subTree")
for(let element of DomTraversal(subTree)) {
console.log(element.nodeName)
}
</script>

通过 next 方法向生成器发送值

生成器不仅可以通过 yield 表达式生成一系列值,还可以接受用户传入数据,形成一种双向的通信。

1
2
3
4
5
6
7
8
9
10
11
function* NinjaGenerator(action) {
const imposter = yield ("Hattori " + action)
yield ("Yoshi (" + imposter + ") " + action)
}

const ninjaIterator = NinjaGenerator("skulk")
const result1 = ninjaIterator.next()
console.log(result1.value) // Hattori skulk

const result2 = ninjaIterator.next("Hanzo")
console.log(result2.value) // Yoshi (Hanzo) skulk

yield

具体的执行流程为:

  • 第一个 ninjaIterator.next() 向生成器请求新值,获取到第一个 yield 右侧的值 "Hattori " + action,同时在 yield ("Hattori " + action) 表达式处挂起执行流程
  • 第二个 ninjaIterator.next("Hanzo") 继续向生成器请求新值,同时还发送了参数 Hanzo 给生成器,该参数刚好用作前面挂起的 yield ("Hattori " + action) 表达式的结果,使得 imposter 的值成为 Hanzo
  • 最终 ninjaIterator.next("Hanzo") 请求获得第二个 yield 右侧 "Yoshi (" + imposter + ") " + action 的值,即 Yoshi (Hanzo) skulk

参考资料

Secrets of the JavaScript Ninja, Second Edition