从一个只在别人电脑上复现的 bug 说起
写过快捷键功能的人大概都遇到过这种情况:功能在自己机器上一切正常,同事拿过去一试就失灵。排查半天,问题既不在业务逻辑也不在浏览器版本,而在最开始那一行判断——用了 e.keyCode === 65。
键盘事件对象上同时挂着 key、code、keyCode 三个属性,看起来都在回答「用户按了哪个键」,但它们描述的根本不是同一件事。选错一个,代码在特定输入法、特定键盘布局下就会失效。
三个属性各描述什么
key 描述的是字符值,也就是这次按键在当前环境下「打出了什么」。英文状态下按 A 键得到 "a",按住 Shift 得到 "A",中文输入法组字过程中则可能得到 "Process"。它的取值随输入法状态和修饰键变化。
code 描述的是物理键位,也就是「用户按下了键盘上的哪个位置」。QWERTY 键盘左上角第一个字母键永远是 KeyQ,与当前输入什么语言、是否开了大写锁定都无关。
keyCode 是早期 DOM 事件规范里的一个数字编码,从未被正式标准化。同一个字母键,在 keydown 里是 65,在 keypress 里却变成 97;小键盘数字键在不同浏览器下也可能给出不同结果。后期新增的多媒体键、Copilot 键干脆没有分配码值,一律返回 0。
一个例子看清区别
假设用户在中文输入法下按下字母 A 键:
| 属性 | 取值 | 原因 |
|---|---|---|
e.key | "a" 或 "Process" | 取决于输入法是否正在组字 |
e.code | "KeyA" | 物理位置固定不变 |
e.keyCode | 65 | 遗留编码,仅供对照历史代码 |
再换一个场景:法国常用的 AZERTY 键盘,左上角第一个字母键上印着 A,但它的物理位置和 QWERTY 的 Q 相同。按下这个键时,e.code 是 "KeyQ",而 e.key 是 "a"。
两个属性给出的答案完全相反,而且都「对」——它们只是回答了不同的问题。
怎么选
判断标准只有一条:你要问的是「用户想输入什么」,还是「用户按了哪里」?
需要识别用户意图时用 key。快捷键、搜索框的回车提交、Esc 关闭弹窗,这些场景关心的是语义,用 key 最直接。
需要识别物理操作时用 code。游戏里的 WASD 移动、音游的键位判定、虚拟钢琴键盘,这类场景关心的是手感一致性,必须用 code。否则 AZERTY 用户会发现角色往奇怪的方向跑。
keyCode 只在一个场景下还有价值:读懂和排障历史代码。新项目不应再引入它。
几个容易踩的细节
修饰键不要用 key 判断。 想确认用户是否按住了 Ctrl,不要去比对 e.key === 'Control',应该直接读 e.ctrlKey、e.shiftKey、e.altKey、e.metaKey 这四个布尔值。它们在每次键盘事件里都会带上,比手动追踪按下抬起状态可靠得多。
preventDefault() 有连带效应。 它会一并拦掉浏览器自身的快捷键。如果在 keydown 里无条件阻止所有 Ctrl 组合键,用户会发现自己按 Ctrl+C 复制不了、按 Ctrl+F 搜不了页面。正确做法是只拦你真正处理了的那几个组合。
输入法组字期间事件会照常触发。 中文、日文输入法在组字过程中,keydown 会触发,但 e.key 是 "Process"。如果此时把字母键当作快捷键处理,用户打字打到一半就会弹出面板。判断方式是配合 compositionstart 与 compositionend 记录组字状态,组字期间直接跳过快捷键逻辑。
小键盘与主键盘的数字键需要区分时看 code。 两者的 keyCode 相同,但 code 一个是 Numpad1、一个是 Digit1。财务录入、收银系统这类场景要求小键盘数字键与主键盘行为不同时,只有 code 能区分。
迁移老代码的检查清单
- 所有
keyCode比较是否都已替换为key或code - 涉及方向、移动、键位判定的逻辑是否改用了
code - 修饰键判断是否已换成
ctrlKey这类布尔属性 - 是否有
preventDefault()覆盖了浏览器默认快捷键 - 是否处理了输入法的
compositionstart/compositionend
把这五处理清,键盘交互在跨平台、跨布局、跨输入法环境下基本就不会再出问题。