
本文详解 tetris 示例中 `moves[event.key](piece)` 的执行机制,包括计算属性名、箭头函数语法、对象展开运算符(`...p`)如何实现“不可变更新”,并说明为何该设计既保持了类实例状态的可控性,又避免了直接修改原对象。
在提供的俄罗斯方块代码中,piece 是一个 Piece 类的实例,拥有 x 和 y 两个可变属性。关键在于:移动操作并未直接修改 piece,而是通过纯函数式方式生成新状态对象,再由 piece.move() 显式应用。这种设计融合了函数式编程思想与面向对象控制流,值得深入拆解。
? 计算属性名与对象方法映射
const KEY = { LEFT: 'ArrowLeft' };
const moves = {
[KEY.LEFT]: (p) => ({ ...p, x: p.x - 1 })
};- [KEY.LEFT] 是 计算属性名(Computed Property Name),等价于 'ArrowLeft': ...。它允许用变量动态生成对象键名,提升可维护性。
- 冒号 : 是对象字面量中 键与值的分隔符,此处左侧是键(字符串 'ArrowLeft'),右侧是值(一个箭头函数)。
➕ 箭头函数与隐式返回
(p) => ({ ...p, x: p.x - 1 }) 是一个带单参数的箭头函数:
- 圆括号 (p) 不可省略(即使只有一个参数,也需括号包裹);
- 箭头 => 后紧跟 ({ ... }) —— 这里必须加括号,否则 { ... } 会被解析为函数体语句块(而非返回对象字面量),导致语法错误或 undefined 返回;
- 括号包裹确保 JavaScript 将其识别为 隐式返回的对象字面量。
? 展开运算符 ...p:安全的浅拷贝更新
{ ...p, x: p.x - 1 } 的含义是:
- 先将 p(即 piece 实例)的所有自有可枚举属性展开复制;
- 再用 x: p.x - 1 覆盖同名属性,形成新对象;
- ✅ 效果等价于:Object.assign({}, p, { x: p.x - 1 });
- ⚠️ 注意:p 是对象引用,...p 仅做浅拷贝——若 p 有嵌套对象,深层属性仍共享引用(本例中 Piece 只含基础类型,无影响)。
? 完整执行流程(以按 ← 键为例)
document.addEventListener('keydown', event => {
if (moves[event.key]) { // event.key === 'ArrowLeft' → 查表命中
event.preventDefault(); // 阻止浏览器默认行为(如页面滚动)
let p = moves[event.key](piece); // 调用函数:传入 piece,返回 {x: -1, y: 0}
piece.move(p); // Piece.prototype.move() 将新坐标赋给 this.x/this.y
}
});? 为什么这样设计?—— 关键优势
- 状态不可变性(Immutable Update):moves 中的函数不修改原始 piece,只产出新状态,便于调试、撤销/重做、时间旅行调试;
- 关注点分离:moves 负责计算新位置(纯逻辑),piece.move() 负责应用变更(副作用),职责清晰;
- 可扩展性强:新增按键只需向 moves 添加键值对,无需改动事件监听器主逻辑。
? 注意事项
- piece 是类实例,但 ...p 展开的是其属性值(x, y),不包含原型方法或私有字段。因此返回的对象是普通 plain object,不能直接调用 move() 方法;
- 若未来 Piece 类增加更多状态(如旋转角度 rotation),只需在 moves 函数中同步更新展开逻辑,例如:{ ...p, x: p.x - 1, rotation: p.rotation }。
这种写法看似“绕路”,实则是现代 JavaScript 应用中平衡可读性、可维护性与函数式原则的典型实践。
立即学习“Java免费学习笔记(深入)”;










