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

微信扫一扫,分享到朋友圈

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
  • laterinnerFunction )执行时, 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 ,令指定元素的宽度能够随时间增长以形成动画效果。该匿名函数借助闭包能够访问外部定义的 elemticktimer 三个参数,控制动画的进度。

这三个参数定义在 animateIt 内部通过闭包被回调函数访问,而不是直接在全局作用域中定义。这样可以避免多个 animateIt 函数依次运行时引起冲突。

三、生成器

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

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

具体的执行流程为:

  • 第一个 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

JavaScript 解密 —— 函数初步

上一篇

熊猫互娱破产拍卖:福袋、周边51元起拍

下一篇

你也可能喜欢

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

长按储存图像,分享给朋友