使用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)