初探 ES6:构建和解决数织
在代码的开头,放置严格模式标识、全局工具函数和常量。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
'use strict'; const sum = array => array.reduce((a, b) => a + b, 0); const deepCopy = object => JSON.parse(JSON.stringify(object)); const eekwall = (object1, object2) => object1.toString() === object2.toString(); // eekwall 长得像 equal,听起来也像 equal,但毕竟不是真正的 equal const FILLED = true; const EMPTY = false; const UNSET = undefined; const TEMPORARILY_FILLED = 1; const TEMPORARILY_EMPTY = -1; const INCONSTANT = 0; |
此时此刻我要记下一点想法,为了提醒未来的自己不进入一个误区,因为下面要说的是一个我非常容易犯的错误。
之前介绍过 mergeSituation
方法的作用。目前它的核心代码如下所示,当然这是好的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
status.forEach((cell, i) => { if (cell === TEMPORARILY_FILLED) { if (this.line[i] === TEMPORARILY_EMPTY) { this.line[i] = INCONSTANT; } else if (this.line[i] === UNSET) { this.line[i] = TEMPORARILY_FILLED; } } else if (cell === TEMPORARILY_EMPTY) { if (this.line[i] === TEMPORARILY_FILLED) { this.line[i] = INCONSTANT; } else if (this.line[i] === UNSET) { this.line[i] = TEMPORARILY_EMPTY; } } }); |
但我通常觉得简短的代码看起来更加舒服:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const FILLED = true; const EMPTY = false; const UNSET = 0; const TEMPORARILY_FILLED = Infinity; const TEMPORARILY_EMPTY = -Infinity; const INCONSTANT = NaN; // ... status.forEach((cell, i) => { if (this.line[i] !== FILLED && this.line[i] !== EMPTY) { this.line[i] += cell; } }); |
这是什么魔法,让代码短了这么多?如果你熟悉 Infinity
、-Infinity
和 NaN
的运算规则,就会发现这两种方式的作用是一模一样的。那为什么这样反而不好呢?一个次要原因是 NaN
。众所周知,NaN === NaN
是假,那假如我要判断一个 cell
是不是 INCONSTANT
,就必须写 isNaN(cell)
而不是 cell === INCONSTANT
,后者会返回一个不期望的结果。主要的原因是代码强耦合,单看 mergeSituation
看不到它的逻辑,必须结合常量具体的值来看,而一旦常量的值发生变更,mergeSituation
将失效!这就与设置常量的初衷『消除耦合』相违背了。
回到代码,看基类的构造。ES6 可以用 class
声明类了,然而它只是语法糖,底层还是用原型实现的继承。并且它有个缺点,就是只能在类里写方法,不能写属性。所以我只好混用了原型和类。据说 ES7 将允许把属性写在类里。不要把属性共享在原型上。参见:http://codereview.stackexchange.com/questions/28344/should-i-put-default-values-of-attributes-on-the-prototype-to-save-space/28360#28360
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
class Nonogram { constructor() { return; } getSingleLine(direction, i) { let g = []; if (direction === 'row') { for (let j = 0; j < this.n; j++) { g[j] = this.grid[i][j]; } } else if (direction === 'col') { for (let j = 0; j < this.m; j++) { g[j] = this.grid[j][i]; } } return g; } removeZeroHints() { this.rowHints.forEach(removeZeroElement); this.colHints.forEach(removeZeroElement); function removeZeroElement(array, j, self) { self[j] = array.filter(Math.sign); } } getHints(direction, i) { return deepCopy(this[`${direction}Hints`][i]); } calculateHints(direction, i) { let hints = []; let line = this.getSingleLine(direction, i); line.reduce((lastIsFilled, cell) => { if (cell === FILLED) { lastIsFilled ? hints[hints.length - 1] += 1 : hints.push(1); } return cell === FILLED; }, false); return hints; } checkCorrectness(direction, i) { return this.calculateHints(direction, i).toString() === this[`${direction}Hints`][i].toString(); } getLocation(x, y) { // 获取点击所在区域 } print() { if (this.canvas) { this.printGrid(); this.printHints(); this.printController(); } } printGrid() { // 展示相关 } printCell(status) { // 展示相关 } printMesh() { // 展示相关 } printHints() { // 展示相关 } printController() { // 展示相关 } } Object.assign(Nonogram.prototype, { backgroundColor: '#fff', filledColor: '#999', unsetColor: '#ccc', correctColor: '#0cf', wrongColor: '#f69', meshColor: '#999', isMeshed: false, isBoldMeshOnly: false, boldMeshGap: 5, }); |
三个子类的构造大同小异,以 NonogramSolve
为例。其中 canvas
参数既可以是<canvas>
元素本身,也可以是它的 id
属性。构造函数中的 config
参数是一个对象,用来自定义实例属性。
函数的参数不应该多于三个,剩余的应当被封装为对象。
——《Clean Code》
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
class NonogramSolve extends Nonogram { constructor(rowHints, colHints, canvas, config) { super(); Object.assign(this, config); // 用配置参数赋值 this.rowHints = deepCopy(rowHints); this.colHints = deepCopy(colHints); this.removeNonPositiveHints(); this.m = rowHints.length; this.n = colHints.length; this.grid = new Array(this.m); for (let i = 0; i < this.m; i++) { this.grid[i] = new Array(this.n); } this.rowHints.forEach(row => { row.isCorrect = false; row.unchangedSinceLastScanned = false; }); this.colHints.forEach(col => { col.isCorrect = false; col.unchangedSinceLastScanned = false; }); this.canvas = canvas instanceof HTMLCanvasElement ? canvas : document.getElementById(canvas); if (!this.canvas || this.canvas.hasAttribute('occupied')) { return; } this.canvas.width = this.width || this.canvas.clientWidth; this.canvas.height = this.canvas.width * (this.m + 1) / (this.n + 1); this.canvas.nonogram = this; this.canvas.addEventListener('click', this.click); this.print(); } get success() { return new Event('success'); } get error() { return new Event('error'); } get cellValueMap() { const t = new Map(); t.set(TEMPORARILY_FILLED, FILLED); t.set(TEMPORARILY_EMPTY, EMPTY); t.set(INCONSTANT, UNSET); return t; } // 其他方法 } |
下面来看看最难的一部分,getAllSituations
的实现。前面说了,它的作用是对于给定的行的长度和提示数字,求出所有可能的填充状态。那么这些状态应该怎么描述呢?还是用前面的例子,提示数字为 2 和 3,长度为 7:
|█|█|X|█|█|█|X| ---- blanks: [0, 1]
|█|█|X|X|█|█|█| ---- blanks: [0, 2]
|X|█|█|X|█|█|█| ---- blanks: [1, 1]
我们仔细观察可以发现,每个提示数字对应的方格组左边的空格组的长度组成的数组,与每种情况一一对应。两个数组的长度应相等。假如我们把空格组叫做 blanks
,提示数字叫做 hints
,那么每行的组成从左到右就是:blanks[0]
个空格,hints[0]
个填充,blanks[1]
个空格,hints[1]
个填充……直到数组结束,若长度未满,仍以空格填充。所以这个问题转化为:求一个整数数组 blanks
,长度等于 hints
的长度,首位不小于 0,其余位不小于 1,且元素之和加上提示数字之和不大于行的长度。用名词来概括,就是深度优先搜索。方法调用与构造如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
this.getAllSituations(this.line.length - sum(this.hints) + 1); // ... getAllSituations(max, array = [], index = 0) { if (index === this.hints.length) { this.blanks = array.slice(0, this.hints.length); this.blanks[0] -= 1; return this.mergeSituation(); } for (let i = 1; i <= max; i++) { array[index] = i; this.getAllSituations(max - array[index], array, index + 1); } } |
剩余部分的实现并不深奥,就不详谈了,感兴趣的读者可以戳GitHub 继续看源代码。似乎还没说这个项目到底怎么玩?GitHub 里也有介绍;如果觉得赞,请在 GitHub 上喂一颗星星!
啊好长好长好长
二维码的演示效果超级炫酷!对,我就是那种看不懂代码只能看看效果的 (*@ο@*)
肖大神怎么可能看不懂代码😁