初探 ES6:构建和解决数织
戳GitHub 可以看到两份源代码,其中一份是 ES5 版本,另一份是 ES6 版本。虽然目前火狐 45 已经涵盖了用到的 ES6 语法了,但为了照顾大多数浏览器,本文包括 GitHub Pages 里的 demo 运行的都是 ES5 版本的。
比较两个版本,一个显著的差别是 ES5 版的代码都写在一个匿名函数里,而 ES6 版没有。有人问这样难道不会污染全局空间吗?答案是不会,因为 ES6 引入了模块的概念。作为一个模块,它并不是通过<script>
标签来引入,而是在另一份文件中通过 import
语句来导入所需要的方法和函数。这点是不是和 Python 很像?
‘use strict’;
历史上有过『少写了一个 var
而弄崩了一个网站』的例子。开发者用 Node.js 做服务端,在某个函数中不用 var
声明变量就使用,于是当多个用户共同访问网站时,该变量被绑定在了全局空间上,导致用户数据相互干扰。那么如何防范这一点呢?用 JSLint 等工具审查代码当然也不错,但我将要介绍严格模式。
严格模式并不是 ES6 的新内容,它在 ES5 时代就被引入了。开启严格模式的方法为:在代码顶部或者函数内顶部放置'use strict';
。严格模式有着更少的容错率,即非严格模式下能正常执行的语句在严格模式下可能会抛出错误。看起来像自讨苦吃?那我来说说它的好处。
JavaScript 是一种对开发者相当友好的弱类型语言,有很多内置方法即使出现了内部错误,也不会报错,取而代之的是静默失败。 多数情况下,它会让你免于处理各式各样的错误;然而事物都是有两面性的,当你的项目出现 bug 却迟迟找不到原因时,它有可能就是罪魁祸首。而严格模式让所有静默失败的操作都抛出错误。像我在上一篇文章里提到的,越能坚持严格的代码风格,就越能回避语法陷阱。关于严格模式的更多介绍,参见 MDN。
严于律己。
——中国先贤
抽象
有了之前的铺垫,就可以专注于实现了。想想我要做什么?一种自动解答的数织(NonogramSolve
),一种用来玩的数织(NonogramPlay
),还有一种能直接编辑网格的数织(NonogramEdit
),用来创建各种图案,让人或者机器来解。当然,它们都叫数织,就有一些公共的属性。
我用:
grid
来表示数织的网格,是一个二维数组;对于每一个元素,用不同的常量标识它的状态;rowHints
来表示所有行的提示数字;colHints
来表示所有列的提示数字。
为了后面使用方便,再用:
m
来表示网格的高度,即行数rowHints.length
;n
来表示网格的宽度,即列数colHints.length
。
它们也有一些公共的方法,比如说展示部分的。这部分将放到它们的基类中。我创建了一个叫做 Nonogram
的基类,用来存放展示方法。抛开展示部分不谈,NonogramPlay
和 NonogramEdit
两个类几乎不剩下什么了。
我们先来看 NonogramSolve
。细致地梳理一遍解答步骤:
- 将所有步骤封装到
solve
方法里。 - 从第一行开始解起,解完之后顺延到下一行。如果这行已被完全解完,即不存在未确定的方格,那么作个标记,下次来到这里时不必花时间再解一遍。
- 所有行解完之后,解第一列;所有列解完之后,解第一行。用函数
updateScanner
控制行列的流程。如果连续地解完了所有的行列,没有一个网格发生变化,那么可以让过程结束,因为即使继续解也不会产生变化。 - 用方法
solveSingleLine
封装对每一行或者列的解答过程。传入两个参数,方向和行列数。将这两个参数传给工具方法getSingleLine
,得到该行列的网格状态和提示数字。 - 如果网格中不存在未确定的方格,那么用工具方法
checkCorrectness
判断网格和提示数字是否一致。如果是,标记为已解决,否则报错。 - 按照提示数字,用
getAllSituations
方法取得网格所有可能的分布情况。对于每一种情况,用mergeSituation
试着将它整合到先前的结果中。当所有情况都处理完之后,用setBackToGrid
将解答结果放回网格中去。
最后一步还没有详细说明。我们得先看看单个网格在解答过程中的所有状态。当刚刚获取该行列的网格时,网格中可能存在三种状态:
FILLED
:已填充;EMPTY
:已标空;UNSET
:未确定。
其中前两种状态都是某种最终状态,不论如何都不会变化。那么在通过 getAllSituations
方法取得若干种情况时,每种情况又包含了已填充和已标空的各种分布,而这种已填充和已标空必须能被后面的情况所覆盖。所以还要新增另外三种状态:
TEMPORARILY_FILLED
:暂时填充;TEMPORARILY_EMPTY
:暂时标空;INCONSTANT
:已出现矛盾。
现在可以说明 mergeSituation
的作用了:每当传入一种情况,与现有的结果进行整合。规则为:
- 如果出现
TEMPORARILY_FILLED + EMPTY
或TEMPORARILY_EMPTY + FILLED
,则跳出; UNSET + TEMPORARILY_* = TEMPORARILY_*
;TEMPORARILY_FILLED + TEMPORARILY_FILLED = TEMPORARILY_FILLED
;TEMPORARILY_EMPTY + TEMPORARILY_EMPTY = TEMPORARILY_EMPTY
;TEMPORARILY_FILLED + TEMPORARILY_EMPTY = INCONSTANT
。- 网格中的
FILLED
、EMPTY
、INCONSTANT
都是最终情况,不再变化。
当所有中可能情况都被处理完,执行 setBackToGrid
方法,三种临时状态要被转换为一般状态。TEMPORARILY_FILLED
对应 FILLED
、TEMPORARILY_EMPTY
对应 EMPTY
、INCONSTANT
对应 UNSET
。
所有流程就是这样。另外,有句话叫做『永远不要信任用户的输入』,假如用户给的提示数字根本就没有解,那应该怎么办?当然有没有解不是一开始就能看出来的,不过可以在方法中多加一点判断,来处理这种特殊情况。
宽以待人。
——中国先贤
在执行 getAllSituations
之前,设置某个表示错误的变量为真。执行 mergeSituation
时,如果未出现 TEMPORARILY_FILLED + EMPTY
或 TEMPORARILY_EMPTY + FILLED
的冲突,就把那个变量设为假。这样就能完美地处理所有合理或不合理的输入了。
我们再来看看 NonogramEdit
。这个类的功能相当简单,事实上它的代码也是最短的。它可以按照给定的宽、高、阈值随机生成一个数织;按照网格计算出提示数字;以及响应点击事件,切换被点击方格的状态。按下不表。
最后来看看 NonogramPlay
。它的功能只有一个,即响应玩家的点击、拖动事件,然而操作方法却很讲究。我参考了许多数织游戏,选择了一种最自然合理的,然而实现起来略显复杂的操作方式:给玩家两种笔刷,『填充』和『标空』;假如玩家选择了『填充』笔刷,第一笔画在的格子状态为:
- 已标空,那么程序不会做出任何响应;
- 未确定,那么笔刷模式为『画笔』,即把接下来碰到的未确定的方格都变为已填充,已标空的和已填充的保持不变;
- 已填充,那么笔刷模式为『橡皮』,即把接下来碰到的已填充的方格都变为未确定,已标空的和未确定的保持不变。
另外,笔刷能影响的范围也有所限制,即它只能影响接触的第一格和第二格所连成的线上。反过来,假如玩家选择了『标空』笔刷,那么将刚才规则的『已标空』和『已填充』字样对换,就是『标空』笔刷的规则。换句话说,这两种笔刷的地位是对等的。
三种类的功能都分析完了,下面可以着手构建基类 Nonogram
了。基类用来存放工具方法、展示相关方法 。
(下一页:实现)
啊好长好长好长
二维码的演示效果超级炫酷!对,我就是那种看不懂代码只能看看效果的 (*@ο@*)
肖大神怎么可能看不懂代码😁