一个ASI陷阱

使用es6学canvas游戏开发

最新在看一本书《HTML5+JavaScript动画基础》, 基于这本书来学习Canvas游戏制作的。书很不错,介绍了很多游戏的基础概念。不过书里面的代码都是基于ES5编写的,现在是2018年了,所以打算边看边改写书里面的代码为ES6版本的。

前天晚上在做ch03/01-rotate-to-mouse.html这个例子,当时的代码大致如下:

    import {captureMouse} from '../include/utils.js'
    import Arrow from './classes/arrow.js'
    window.onload = () => {
      const canvas = document.getElementById('canvas')
      const context = canvas.getContext('2d')
      let mouse = captureMouse(canvas)
      let arrow = new Arrow()

      (function drawFrame () {
        window.requestAnimationFrame(drawFrame, canvas)
        context.clearRect(0, 0, canvas.width, canvas.height)
        const dx = mouse.x - arrow.x
        const dy = mouse.y - arrow.y
        arrow.rotation = Math.atan2(dy, dx)
        arrow.draw(context)
      }())
    };

诡异的问题

上面看着是没有任何问题的,可是执行的时候,一直报错:

Uncaught ReferenceError: arrow is not defined

当时就有点懵,没道理啊,drawFrame跟arrow的定义在同一个作用域,那drawFrame函数内部作用域里面肯定是能访问到外部作用域的arrow的,怎么可能没定义

当时有点怀疑自己是不是ES6没学好,这种IIFE的自执行函数难到在严格模式下有什么特殊的行为?

为了证实自己的猜测,改写了一下代码:

    import {captureMouse} from '../include/utils.js'
    import Arrow from './classes/arrow.js'
    window.onload = () => {
      const canvas = document.getElementById('canvas')
      const context = canvas.getContext('2d')
      let mouse = captureMouse(canvas)
      let arrow = new Arrow()

      function drawFrame () {
        window.requestAnimationFrame(drawFrame, canvas)
        context.clearRect(0, 0, canvas.width, canvas.height)
        const dx = mouse.x - arrow.x
        const dy = mouse.y - arrow.y
        arrow.rotation = Math.atan2(dy, dx)
        arrow.draw(context)
      }
      drawFrame()
    };

果然,代码顺利运行了。

然后我就以es6, scope, function, iife, variable, let这几个关键词苦苦Google,查了一堆网页,可就是没找到到底这个作用域是怎么影响的

为看缩小问题范围,于是重新写了一个demo:

    function foo(){
      var a = 1
      let b = 2
      (function bar() {
          console.log(a)
          console.log(b)
      }())
    }
    console.log(foo());

结果a能被正确log出来,b还是not defined

这下更证明自己的猜想了。可是其中的原理还是不懂

stackoverflow上的大牛

无奈之下,在stackoverflow上面发起了一个问题:a variable defined with let is not defined in a same scope IIFE

很快就有一个热心的大牛T.J. Crowder帮忙给编辑了一下问题,修复了一些语法上的错误,优化了代码展示。

然后这位大牛又顺便给解答了问题

答案跟我一直猜想的方向完全不一致,一切都是ASI(Automatic Semicolon Insertion)导致的。demo里面的代码,在实际被解析的时候,是大致长这样的:

    function foo(){
      var a = 1
      let b = 2(function bar() {
        console.log(a)
        console.log(b)
      }());
    }
    console.log(foo());

b的赋值和iife的执行连接到一起了,这也就是为什么函数体内b是not defined的原因,iife的执行先于b的定义

深入理解

关于ASI,是有了解的,不过这个知识点在我脑海里是跟代码压缩绑定在一起。初学js的时候,遇到过分号缺失导致的压缩代码执行错误,所以在js文件里面写代码的时候,会很注意这方面。

而这次是在html的script标签里面写代码,想着这些代码又不会被手动压缩,自然就没想过ASI的问题。

可实际上,任何js代码在解析执行的时候,都会在必要的时候经由解析器执行ASI来”补全分号”

总结

只要你在写js, 不管是在js文件里面还是script标签里面,分号都是一个值得严肃对待的事情

关于ASI的详细描述,可以参看以下两篇文章(我也是刚刚看的):

备胎的自我修养——趣谈 JavaScript 中的 ASI (Automatic Semicolon Insertion)

JavaScript ASI 机制详解