WebGL Tutorial
and more

键盘事件

撰写时间:2024-11-06

修订时间:2025-02-01

ASCII码表基本知识

计算机内部使用二进制的数值来编码。为确定各个字符在计算机存储时的唯一编码,美国先于1967年提出其自己的使用数值来代表字符的ASCII编码表,全称为American Standard Code for Information Interchange,即美国信息交换标准码。后经ISO 646确定为国际标准。

上世纪六十年代,人们经常在打电话或使用异地打印机时使用ASCII码表来传送文本信息。因此,ASCII码表中有一部分称为控制编码,如编号为2开始正文,编号为4传输结束等等。这些古老的用途,现在当然用不上了。但这一部分现在看来奇奇怪怪的编码继续保存于ASCII码表中。

从发展阶段,ASCII码表分为ASCII基础码,及ASCII扩展码。

ASCII基础码

最早的时候,人们认为使用7位的编码方案就足够了。27 = 128,因此这种编码方案下共有128个字符。

ASCII控制编码

编号031,以及编号127代表控制字符,也称为不可打印字符

ASCII控制编码
十进制编号十六进制编号符号标识英文含义中文含义
00X00NULNull空字符
10X01SOHStart of Heading标题开始
20X02STXStart of Text正文开始
30X03ETXEnd of Text正文结束
40X04EOTEnd of Transmission传输结束
50X05ENQEnquiry查询
60X06ACKAcknowledge感谢
70X07BELBell响铃
80X08BSBackspace回删
90X09HTHorizontal Tab水平跳格
100X0ALFLine Feed换行
110X0BVTVertical Tab垂直跳格
120X0CFFForm Feed开始制表,另起新页
130X0DCRCarriage Return回车
140X0ESOShift Out
150X0FSIShift In
160X10DLEData Link Escape
170X11DC1Device Control 1设备控制1
180X12DC2Device Control 2设备控制2
190X13DC3Device Control 3设备控制3
200X14DC4Device Control 4设备控制4
210X15NAKNagative Acknowledge不谢
220X16SYNSynchronous Idle同步闲置
230X17ETBEnd of Transmission Block传输块结束
240X18CANCancel取消
250X19EMEnd of Medium中部结束
260X1ASUBSubstitute替换
270X1BESCEscape退出
280X1CFSFile Separator文件分隔
290X1DGSGroup Separator分组
300X1ERSRecord Separator记录分隔
310X1FUSUnit Separator单元分隔
1270X7FDELDelete删除

设想,在上世纪六十年代,甲通过电话指导乙使用机械打字机打印文件时,使用了上面的标识符,您就明白这些编码存在的意义了。

可打印的基础码

ASCII基础码中剩下的编码,也即从32126,均为可打印出来的字符。

ASCII扩展码

使用7位来存储编码只能表示128个编码,因此后面又出了使用8位的扩展编码,能表示28 = 256个字符。

编号从128255共计128ASCII编码为扩展码,为一些制表符、货币符号、数学符号及希腊字母等等。

但仅增加128个字符也同样不足以表示各种编码,因此,不同国家、地区、系统均出了不同的代码页(code page),以表示各不相同的字符。因此,ASCII扩展码因系统所使用的不同的代码页而有所变化。且不同的浏览器,对于相同的代码页,支持力度也不一样,从而导致一些字符无法显示出来。这些乱象,直接呼唤了Unicode编码的出现。

Unicode编码又分为UTF-8, UTF-16, UTF-32等方式。因此,字符编码的完全标准化是一个时间较长的问题。

因此,对于ASCII扩展码,我们一般较少使用。若要使用,则应使用Unicode的方式。

Unicode及码位

很明显,ASCII码表因位数的限制,不能表示世界上所有的字符。Unicode应运而生。Unicode使用code point (码位)来唯一标识每个字符。

特定字符的码位是该字符在Unicode表中的编码位置。码位类似于ASCII值,但ASCII值是字符在ASCII码表中的位置,而码位是该字符在Unicode表中的位置。

可将Unicode视为ASCII的超集。Unicode是后面才设计的,为往前兼容,ASCII码表保持不变。因此,对于ASCII码表中的基础码,其码位与其ASCII值一样。

在印刷媒介、网络媒介上使用Unicode来表示一个字符的格式如U+9AD9,其数字部分采用十六进制,代表汉字字符。则9AD9即为该字符的码位

码位与字符的相互转换

根据Unicode的code point来获取字符

既然每个码位能唯一标识每个字符,那么我们就能根据不同的码位来获取不同的字符。

JavaScript获取

在JavaScript中,要根据这个码位来获取相应的字符,共有2种方式:

第一种是直接使用字面符的方式:

let char = "\u9AD9"; console.log(char); // 高

首先,JavaScript使用\u而不是U+作为前缀,这与印刷媒介、网络媒介上的通用表示方法不同。其次,上面我们不能写成"\u" + "9AD9"的形式。因为JavaScript将\u视为一个独立的转义字符,且后面必须直接带有代表码位的字符串(4位大小写均可的十六进制数值,不足4位,前面须补0)。

在一个字符串中同时使用多个Unicode字符:

let str = "\u9AD9\u9ADA"; console.log(str); // 髙髚

如果上面每个Unicode字符之间带有空格,则空格也成为字符串的一部分内容。

let str = '\u9AD9 \u9ADA'; console.log(str); // 髙 髚

对于超出BMP平面外的字符,可以使用如下语法:

let char = "\u{1F60A}"; console.log(char); // 😊

上面的表示方法是通用的,照样可适用于码位为两字节的字面符。因此,也可表示为:

let char = "\u{9AD9}"; console.log(char); // 高

这种表达方式比较自由,而且十六进制数值若不足4位,无须在有前补0:

let char = "\u{53}"; console.log(char); // S

如果希望将上面的码位分离出来以灵活处理不同的码位,则可使用第二种方法。这种方法通过调用String的静态方法fromCodePoint来获取字符:

let codePoint = 0X9AD9; let char = String.fromCodePoint(codePoint); console.log(char); // 高

一并获取多个字符:

let codePoints = [0X9AD9, 0x9ADA]; let str = String.fromCodePoint(...codePoints); console.log(str); // 髙髚

fromCodePoint方法的参数是可变长的参数,因此上面使用...展开操作符将数组展平为独立的多个个体。

HTML获取

而在HTML中,则可使用

<p>&#X9AD9;</p>

的方式来代表此字符。

利用本站编写的HTML Unicode工具,选择4E00-9FBF这个范围,则可看到此字符出现在9AD0一行的第9列中。而表上可显示出,4E00-9FBF这个范围是CJK统一表意符号,也即中日韩最常用的汉字区域(CJK = Chinese, Janpanese, and Korean)。

此外,在 MacOS X 系统中,可按⌃Control+⌘Command+Space来打开字符检视器 App,选自定义列表...,也可选择查看所有的Unicode

获取特定字符的码位

获取

对于特定字符,如果我们想知道该字符的Unicode码位,则可调用StringcodePointAt方法:

let char = "高"; let codePoint = char.codePointAt(0); console.log(codePoint); // 39640 [type: number]

所得到的结果39640是码位的的十进制表示。转换为十六进制:

console.log(codePoint.toString(16)); // 9ad8

有趣的应用

从上面可知,一些字符已经置于Unicode表中,例如:😊。

确实很酷。但我还想知道,除了它之外,Unicode表中还有多少个类似的图标可供我选择使用?答案是,从码位寻求解决问题的方法。

第一步,求出该字符的码位。好消息是,Unicode字符在代码中可以直接复制、粘帖。

let char = "😊"; let codePoint = char.codePointAt(0); console.log(codePoint.toString(16)); // 1f60a

第二步,在该码位上进行简单的加减,再获取新的字符:

codePoint++; let newChar = String.fromCodePoint(codePoint); console.log(newChar); // 😋

两个字符的区别很小,新字符往左吐出了舌头。

有了上面的基础,我们就可以编写一个实用的函数了:

function printCharsInRange(char, prev = 5, next = 5, gap = 5) { let codePoint = char.codePointAt(0); let startCP = codePoint - prev; let endCP = codePoint + next; let str = ""; for (let i = startCP; i < codePoint; i++) { let newChar = String.fromCodePoint(i); str += newChar; } str = str + ' '.repeat(gap) + char + ' '.repeat(gap) ; for (let i = codePoint + 1; i <= endCP; i++) { let newChar = String.fromCodePoint(i); str += newChar; } console.log(str); } printCharsInRange("😊", 10, 10);

运行结果:

😀😁😂😃😄😅😆😇😈😉 😊 😋😌😍😎😏😐😑😒😓😔

参数prev表示在当前字符的前面取多少个字符,next表示在当前字符的后面取多少个字符,gap用于控制当前字符与其他字符的空格间距。

Unicode码表实在是太大了,我们不好一个一个地搜寻特定的字符。但如果我们在网络上看到特定的字符,利用相似字符在Unicode码表中集中排列的特点,通过上面的小工具就可以一下子就可搜索出所有类似的表情字符。

当然,如果您喜欢来现成的,则也可以在Emoji Sequences找到所有的表情字符,或者图形索引版,或者更漂亮的图形版

Unicode与编码的关系

Unicode只是简单地规范了哪个字符对应于哪个码位,如,笑脸字符😊对应于码位0X1F60A。它并不规范在计算机内部如何存储、如何取出并解码。

上面我们多次调用的fromCodePoint仅告诉我们特定字符的码位是多少,但它并不告诉我们在计算机内部使用何种机制来存储。

Unicode编码就专注于解决诸如使用应多少个字节来存储每个字符、这些字节如何排列、如何正确地取出一个字符所需的所有字节并解读为特定字符的问题。

目前主要有3Unicode编码:UTF-8, UTF-16, UTF-32

UTF-8是可变长的编码,分别使用14个字节来表示各个字符。

UTF-16也是可变长的编码,但它只使用2字节或4字节来表示各个字符。JavaScript使用UTF-16来存储字符串。

UTF-32统一使用4个字节来存储。

下面结合JavaScript的语言特性,谈谈其对各类编码的支持。

JavaScript对UTF-8的支持

JavaScript通过TextEncoder类,提供了通用的UTF-8的支持。该类将传入的码位流转换为由众多的UTF-8字节所组成的流。

let encoder = new TextEncoder(); const view = encoder.encode("A"); console.log(view); // Uint8Array [65]

TextEncoder的构造函数没有任何参数,意味着它的输出结果只有UTF-8一种。因此,其属性encoding永远只有一个值:

console.log(encoder.encoding); // "utf-8"

代码:

const view = encoder.encode("A"); // Uint8Array [65]

encoder对字符串A(虽然这里只有一个字符)使用UTF-8进行编码,并将结果保存进view中。变量view的类型是Uint8Array,即无符号8位整数的数组。

上面代码可以看出,对于字符A,其UTF-8编码只有一个字节,其值为65。这个值对应于ASCII值,或码位

我们还可以调用encoderencodeInto方法,将编码结果存储进指定的Uint8Array中。

let char = "天"; let encoder = new TextEncoder(); let targetArr = new Uint8Array(8); const result = encoder.encodeInto(char, targetArr); console.log(targetArr); // Uint8Array [229, 164, 169, 0, 0, 0, 0, 0] [8] console.log(result); // {read: 1, written: 3}

targetArr看出,这个汉字共有3个字节,分别为229, 164, 169result提供了额外的信息:从JavaScript所使用的UTF-16编码转换为UTF-8编码单元 (code unit,详见下面)数量为1,目标数组中被修改的字节数量为3

UTF-16

JavaScript的字符串使用UTF-16编码。Char code是内存中存储的数据,是据以计算码位的依据。

何为Char Code

Char code不是指字符编码,而是指字节码。什么意思?一步一步来。

首先,还是取上面的笑脸为例。

let char = "\u{1F60A}"; console.log(char); // 😊 let codePoint = char.codePointAt(0); console.log(codePoint.toString(16)); // 0X1F60A

笑脸的码位0X1F60A。先记住这一点。

调用charCodeAt方法,分别取出该字符的2char codes。

let charCode1 = char.charCodeAt(0); console.log(charCode1.toString(16)); // 0XD83D let charCode2 = char.charCodeAt(1); console.log(charCode2.toString(16)); // 0XDE0A

第三步,进行神奇的运算:

let result = (charCode1 - 0xD800) * 0x400 + (charCode2 - 0xDC00) + 0x10000; console.log(result.toString(16)); // 0X1F60A

发现了吗?变量codePoint的值等于result的值,它们都代表了该字符的码位!

将Char code转换为UTF-16编码

上面的charCode1charCode2,其实就是JavaScript使用UTF-16编码来存储笑脸字符😊的在内存中的字节数据。我们通过实践来验证:

let bytesArr = new Uint16Array([0XD83D, 0XDE0A]); let decoder = new TextDecoder('utf-16'); let str = decoder.decode(bytesArr); console.log(str); // 😊

通过硬编码的方式,将charCode1charCode2的值用于构建一个Uint16Array,然后使用utf-16对该数组中的字节流进行解码,得到了我们预期的效果。

转换细节

仍以上面类型化数组数据为例。一是如果数组元素的值小于等于0XFFFF,则直接取该元素值作为该字符的码位而予以解析。

let bytesArr = new Uint16Array([0x2615]); let decoder = new TextDecoder('utf-16'); let str = decoder.decode(bytesArr); console.log(str); // ☕

0x2615的值小于0XFFFF,则它就是字符码位,直接解析。

Unicode规范规定:0xD800 - 0xDBFF此段为leading surrogate(开始代理段,也称high-surrogate code unit,高位代理编码单元);0xDC00 - 0xDFFF此段为trailing surrogate(结尾代理段,也称low-surrogate code unit,低位代理编码单元)。这两段将用于映射而予以保留。

由此在JavaScript内产生第二条规则:如果遇到两个数值,第1个数值c1位于0xD800 - 0xDBFF范围内,且第2个数值c2位于0xDC00 - 0xDFFF范围内,则该字符的码位为:

let codePoint = (c1 - 0xD800) * 0x400 + (c2 - 0xDC00) + 0x10000

这就是上面我们的代码:

let result = (charCode1 - 0xD800) * 0x400 + (charCode2 - 0xDC00) + 0x10000;

的由来。

JavaScript使用UTF-16解码的第三条规则是,如果一个编码单元位于高位代理编码单元低位代理编码单元,但又无其他数值与其构成代理对,则直接取其值为码位

由此可见,char codeUTF-16编码在内存中实际存储的数据,charCodeAt方法可获取这些数值,它们是JavaScript据以计算码位的依据。

响应键盘事件

下面代码,在按下特定键时,在网页上看到keycode属性值,在console面板中看到事件参数。

let body = document.body; let output = document.querySelector('output'); body.onkeydown = (evt) => { evt.preventDefault(); output.insertAdjacentHTML('afterbegin', `

"${evt.key}", "${evt.code}"

`); console.dir(evt); };

参数evtKeyboardEvent的一个实例。与键码输入有关的属性有:

当我们输入A时:

  • charCode: 0
  • code: "KeyA"
  • key: "a"
  • keyCode: 65
  • keyIdentifier: "U+0041"
  • which: 65

上面,charCode, keyCodekeyIdentifier这3个属性已被废弃。

注:codeQWERTY的键盘布局返回键码。对于一些布局为QWERTZ的键盘,当用户按下Z键时,code仍返回Y

综上,对于上面众多的属性,选用key属性,较为稳妥。但code属性值也包含了一些有用的信息。

keycode属性值一致的铵健
按钮key属性值code属性值
InsertHelpHelp
DeleteDeleteDelete
HomeHomeHome
EndEndEnd
Page UpPageUpPageUp
Page DownPageDownPageDown
EnterEnterEnter
BackspaceBackspaceBackspace
EscEscapeEscape
F1 ... F10F1 ... F10F1 ... F10
Print ScreenF13F13
ArrowLeftArrowLeft
ArrowRightArrowRight
ArrowUpArrowUp
ArrowDownArrowDown
Caps LockCapsLockCapsLock
TabTabTab

keycode属性值不一致的铵健
按钮key属性值code属性值
Spacebar Space
0 ... 90 ... 9Digit0 ... Digit9
a ... za ... zKeyA ... KeyZ

小键盘上的keycode属性值
按钮key属性值code属性值
Num LockClearNumLock
++NumpadAdd
--NumpadSubstract
**NumpadMultiply
//NumpadDivide
EnterEnterNumpadEnter
0 ... 90 ... 9Numpad0 ... Numpad9
..NumpadDecimal

有上档键码的keycode属性值
组别按钮key属性值code属性值
1~~Backquote
``
2__Minus
--
3++Equal
==
4||Backslash
\\
5::Semicolon
;;
6""Quote
''
7<<Comma
,,
8>>Period
..
9??Slash
//

上面均以下档键的名称来标识。

功能键的keycode属性值
按键位置key属性值code属性值设置标志属性
ShiftShiftShiftLeftshiftKey
ShiftRight
ControlControlControlLeftctrlKey
ControlRight
CommandMetaMetaLeftmetaKey
MetaRight
OptionAltAltLeftaltKey
AltRight

按住Option键,可方便地输入一些常用字符。访问key-events.html以测试。

参考资源

  1. unicode.org
  2. Unicode V16
  3. Emoji Sequences
  4. ASCII Table (lookuptables.com)
  5. asciitable.com
  6. ASCII(百科)
  7. 中日韩越统一表意文字(百科)
  8. 统一码(百科)
  9. UTF-8(百科)
  10. Unicode与JavaScript详解 (阮一峰的网络日志)
  11. 字符编码笔记:ASCII,Unicode 和 UTF-8 (阮一峰的网络日志)
  12. ECMA 262, The String Type
  13. MDN keyboardEvent