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

2020-06-11  本文已影响0人  rollingstarky

一、闭包

简单来说,闭包(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

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

实际上程序输出的 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
yield

具体的执行流程为:

参考资料

Secrets of the JavaScript Ninja, Second Edition

上一篇下一篇

猜你喜欢

热点阅读