美文网首页
JavaScript 解密 —— 函数进阶(闭包与生成器)

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

作者: rollingstarky | 来源:发表于2020-06-11 10:27 被阅读0次

    一、闭包

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

    相关文章

      网友评论

          本文标题:JavaScript 解密 —— 函数进阶(闭包与生成器)

          本文链接:https://www.haomeiwen.com/subject/bmxxzhtx.html