JavaScript 解密 —— 函数进阶(闭包与生成器)
一、闭包
简单来说,闭包(closure)允许函数访问和操作位于自身外部的变量。
借助闭包的特性,函数可以访问任何变量及其他函数,只要这些数据在该函数定义时位于其作用域内部。
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
- 当
later
(innerFunction
)执行时,outerFunction
已经执行完毕,其内部的作用域理应失效,无法被later
访问 -
innerValue
由于在outerFunction
内部定义,则later
访问innerValue
时其值应该为undefined
实际上程序输出的 innerValue
的值为 ninja
,即 outerFunction
内部定义的 innerValue
可以被 later
访问。这就是闭包所产生的效果。
当我们在 outerFunction
内部声明 innerFunction
时,一个包含当前作用域(“当前”指的是内部函数定义的时刻)中所有变量的闭包同时被创建。最终 innerFunction
执行时,即便其声明时的原始作用域已经消失,innerFunction
还是可以通过闭包访问其原始作用域。
闭包像是使用了一个“保护层”将函数定义时的作用域封闭起来,只要该函数的生命周期未结束,“保护层”内的作用域就一直可以被访问。
二、闭包的现实应用
模拟私有变量
私有变量即从对象外部不可见的变量,可以向用户隐藏对象内部不必要的实现细节。
JavaScript 没有对私有变量的原生支持,但是通过闭包可以实现类似的功能。
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
在回调函数中使用闭包
<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
,令指定元素的宽度能够随时间增长以形成动画效果。该匿名函数借助闭包能够访问外部定义的 elem
、tick
、timer
三个参数,控制动画的进度。
这三个参数定义在 animateIt
内部通过闭包被回调函数访问,而不是直接在全局作用域中定义。这样可以避免多个 animateIt
函数依次运行时引起冲突。
三、生成器
生成器是一种可以生成一系列值的特殊函数,只不过这些值不是同时产生的,需要用户显式地去请求新值(通过 for
、next
等)。
function* WeaponGenerator() {
yield "Katana"
yield "Wakizashi"
yield "Kusarigama"
}
for(let weapon of WeaponGenerator()) {
console.log(weapon)
}
// Katana
// Wakizashi
// Kusarigama
调用生成器并不意味着会逐步执行生成器函数的定义代码,而是会创建一个迭代器(iterator)对象,通过这个迭代器对象与生成器进行交互(如请求新的值)。
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
遍历生成器:
function* WeaponGenerator() {
yield "Katana"
yield "Wakizashi"
}
const weaponsIterator = WeaponGenerator()
let item
while(!(item = weaponsIterator.next()).done) {
console.log(item.value)
}
// Katana
// Wakizashi
生成器嵌套:
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
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
使用递归函数:
<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):
<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
表达式生成一系列值,还可以接受用户传入数据,形成一种双向的通信。
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