先说几个核心判断:直接用 div + grid 布局,配原生 JS,完全能跑通 2048 的核心逻辑。ChatGPT 写的代码未必比这更优雅。不用框架、不依赖 Canvas,只要 4x4 合并判断写对了,游戏就活了。关键是这六个环节不能出错。
初始化一个空的 4x4 游戏板,怎么写才稳妥?
千万别踩这个坑:Array(4).fill(Array(4).fill(0))。表面看着省事,实际上创建了 4 个指向同一数组的引用——改一行整列全变,游戏逻辑直接崩。正确写法是用 map 每次新建一个新数组:
const board = Array(4).fill().map(() => Array(4).fill(0));
初始化完成后,立刻调用两次 addNumber(),保证开局有两个数字出现。它的逻辑分了三步:先遍历所有格子,把值为 0 的坐标存进 emptyCells 数组;然后用 Math.floor(Math.random() * emptyCells.length) 随机取一个索引(这里必须做空数组检查,否则容易崩溃);最后根据 Math.random() < 0.1 生成 4(占 10%),其余生成 2(占 90%)。
方向移动时,合并逻辑怎么写才不遗漏、不重复?
这个问题是 2048 实现中最容易出 bug 的地方。关键在哪?遍历顺序和合并标记。比如 moveLeft() 必须按行遍历,每行内从左到右扫描,但每个格子最多只能参与一次合并——合并后该位置不能再被后续格子合并。
最常见的错误写法是两层 for 循环里直接写 board[i][j] += board[i][j+1],完全没考虑已合并的格子是否还能动。实践中,更好的做法是:
- 为每行建一个临时数组
row = board[i].filter(n => n !== 0) - 单趟遍历
row,逻辑是:if (row[j] === row[j+1]) { row[j] *= 2; row.splice(j+1, 1); } - 补零至长度 4,再写回
board[i]
上下方向同理,只是把“行”换成“列”,遍历维度换一下方向即可。
游戏结束的判断,缺了哪一项都会出 bug?
很多人以为棋盘满格就意味着游戏结束。不对。满格时还可能有相邻相等数字可合并。必须同时满足三个条件:所有格子非零(board.flat().every(n => n !== 0))、横向无相邻相等、纵向无相邻相等。只有三个条件同时满足,才触发 gameOver。漏掉任何一个,就会出现“明明还能合并却提示 Game Over”的情况——这才是 bug 最常出现的地方。
键盘事件监听,怎么避免卡死或重复触发?
keydown 事件默认会连续触发,长按方向键会导致一次按键执行多次移动。实践经验是加一个防抖变量 isMoving 来标记当前是否正在处理移动:在 handleKeyPress 开头加 if (isMoving) return;,进入 move() 后立刻设 isMoving = true,渲染完、新方块加完再设 isMoving = false。否则玩家猛按方向键,可能一次滑动就把整行全吸到左边,中间根本没机会合并。
还有一个最容易忽略的细节:每次移动后,必须严格区分是否真发生了位置变化或合并。只有 hasMoved === true 才调用 addNumber()。否则空按方向键也会塞新数字,棋盘很快就会爆满。

