二进制与编码
进制转换
// 十进制 -> 二进制
const num = 255;
num.toString(2) // '11111111'
// 十进制 -> 十六进制
const num = 255;
num.toString(16) // 'ff'
// 二进制 -> 十进制
parseInt(0b1111) // 15
// 十六进制 -> 十进制
parseInt(0xff) // 255
位运算
虽然日常编码中不一定会用到位运算,但在某些特定情况下位运算可能意外的好用。假设存在这样的场景,平台下不同用户可能具备不同功能的白名单,因此我们需要使用一个字段features
来记录用户所具备的功能,在使用时根据这个字段来判断用户具备哪些功能。
首先,我们通过一个Feature
对象定义了一组功能,并使用二进制来表示每个功能的值。
const Feature = {
One: 0b01,
Two: 0b10,
Three: 0b100,
Four: 0b1000,
}
那么假设用户具备了所有的功能,那么他的features
字段的值应该是0b1111
。
左移
上述的代码中Feature
的定义有些繁琐,我们可以使用更简洁的写法:
let shift = 0
const Feature = {
One: 1 << shift++,
Two: 1 << shift++,
Three: 1 << shift++,
Four: 1 << shift++,
}
在这个例子中的<<
被称作左移运算符,它表示把二进制的每一位都向左移动指定的位数。如对于1 << 3
表达式,返回的是0b1
左移3位后的结果即0b1000
,也就是十进制的8。
右移
与左移运算符对应的,通过右移运算符>>
可以返回二进制的每一位都右移指定位数的结果。如对于8 >> 3
表达式,返回的是0b1000
右移3位的结果即0b1
,即十进制的1。
按位或
0b0101
表示用户同时具备Feature.One
和Feature.Two
这两个功能,实际上这是这两个变量**按位或|**的结果。
const features = Feature.One | Feature.Three
按位与
为了了解用户是否具备Feature.One
的功能,我们的目的是确认features
的最后一位的值是否为1,通常可以使用**按位与&**来进行区分
const hasFeature = (feature & Features.One) > 0
Buffer
Buffer
用于表示固定长度的字节序列,Buffer
仅存在于Node.js
环境中,在浏览器环境中使用ArrayBuffer
作为替代,二者的原理基本一致但API层面存在些许差异。
通过fs
模块不指定编码方式encoding
读取文件时,我们能够获取到表示二进制数据的Buffer
对象。
Buffer.alloc()
可以用来生成指定长度的Buffer,也可以同时指定Buffer字节序列的值。
const buffer = Buffer.alloc(10) // <Buffer 00 00 00 00 00 00 00 00 00 00>
const buffer = Buffer.alloc(10, 0xfc) // <Buffer fc fc fc fc fc fc fc fc fc fc>
Buffer.from(string[, encoding])
将字符串编码成Buffer
表示的字节序列,默认为utf-8
编码
const buffer = Buffer.from('aka') // <Buffer 61 6b 61>
const buffer = Buffer.from('你好') // <Buffer e4 bd a0 e5 a5 bd>
buffer.toString([encoding])
将Buffer
解码成字符串,默认为utf-8
编码
const buffer = Buffer.from('aka')
const str = buffer.toString() // aka
ArrayBuffer
const buffer = new ArrayBuffer(3) // 创建3个字节长的ArrayBuffer
和Node环境中的Buffer
类似,浏览器环境提供了**ArrayBuffer
数据类型表示一块内存中的字节序列。但我们无法直接操作这块内存,而是需要为ArrayBuffer
创建Typed Array
或者DataView
的视图**,并借助视图的能力来读写对应的字节。我们可以为同一个ArrayBuffer创建多个不同的视图,他们本质上读写的都是同一块内存/同一个ArrayBuffer。
这两种视图的主要差异包括这两点:
- 步长不同。对于特定的
Typed Array
,比如Uint32Array
实例,这个数组的每个索引对应的值都由32位即4个字节组成,我们每次读写都是同时操作固定长度的字节。而DataView
更加灵活,同一个实例即可以调 用getUint8
获取一个字节的内容,也可以调用getUint32
获取四个字节的内容。 - 字节序不同。
Typed Array
采用小端字节序,并且无法修改;而DataView
默认采用大端字节序,但也支持使用小端字节序。
在计算机领域中,一个多字节的数值在内存中的存储顺序会根据我们所采用的字节序策略的不同而产生差异。字节序又分为小端字节序和大端字节序,比如对于一个两字节的数值0x0102来说,如果我们以小端字节序写入到内存中,则内存中的表示为[02, 01];反之若我们以大端字节序的形式,则内存中的表示为[01, 02]。
一般来说,系统内字节的存储都是采用小端字节序,因此我们通常采用小端字节序读取内存数据即可。而网络中传输的字节则是大端字节序,如果需要访问网络流中的数据,需要采用大端字节序的方式才能正确读取内容。
类型化数组(Typed Array)
Typed Array
可以理解成一种抽象类,具体的实现包括Uint8Array
、Int8Array
、Uint32Array
、Int32Array
,以及Uint8ClampedArray
等特化。
const buffer = new ArrayBuffer(3)
const typedArray1 = new Uint8Array(buffer) // length: 3, byteLength: 3
buffer === typedArray1.buffer // true
const typedArray2 = new Uint32Array([65]) // length: 1, byteLength: 4
const typedArray3 = new Uint8Array([65, 66, 67, 68]) // length: 4, byteLength: 4
const typedArray4 = new Uint16Array(typedArray3.buffer) // length: 2, byteLength: 4
typedArray4[0] = 0;
typedArray3[0] === 0 // true;
typedArray3[1] === 0 // true
const buffer = new ArrayBuffer(4)
const typedArray1 = new Uint8Array(buffer)
const typedArray2 = new Uint16Array(buffer)
typedArray2[0] = 255
typedArray1[0] === 255 // true
typedArray1[1] === 0 // true
const buffer = new ArrayBuffer(2)
const typedArray1 = new Uint8Array(buffer)
typedArray1[0] = 256
typedArray1[0] === 0 // true
const typedArray2 = new Uint8ClampedArray(buffer)
typedArray2[0] = 256
typedArray2[0] === 255 // true
DataView
const buffer = new ArrayBuffer(3)
const view1 = new DataView(buffer)
const view2 = new DataView(new Uint8Array([65, 66, 67, 68]).buffer)
const buffer = new ArrayBuffer(4)
const typedArray1 = new Uint8Array(buffer)
const view = new DataView(buffer)
view.setUint16(0, 255)
typedArray1[0] === 0 // true;
typedArray1[1] === 255 // true
view.setUint16(0, 255, true) // DataView也可以指定小端字节序
typedArray1[0] === 255 // true
typedArray1[1] === 0 // true
字符编码
编码与解码
无论是Buffer
还是ArrayBuffer
,本质上都表示着内存中的二进制数据(或者叫字节序列)。借助指定的解码方式,我们可以将其转换为对应的文本字符,常见的编码方式有ascii
、gbk
、utf-8
、utf-16
等。一般的解析函数编码格式都为utf-8
,比如Buffer.from
、fs.writeFile
、fs.readFile
,以及后文会介绍的TextEncoder
、TextDecoder
等。
需要特别注意的一点是在JavaScript中字符串的存储是**utf-16
**的格式,通过charCodeAt
方法我们可以查询某个JS字符在内存中对应的二进制内容。
'A'.charCodeAt(0) // 获取字符解码后的二进制数据
对于无法通过utf-16
解码成正常文本的字符串来说,会用类似\u0003
、\x03
的形式进行表示
'\x03'.length // 1
'\x03'.charCodeAt(0) // 3
字符集
上一节介绍了编码和解码,一个字符通过不同的编码方式可以转换为不同格式的二进制,而除了编码的概念外我们可能还听说过Unicode和字符集。简单来说,每个字符都和对应的码点(CodePoint)存在一对一映射关系,而根据我们采用的编码方式的不同(utf-8
、utf-16
、utf-32
),同一个码点又会对应不同的二进制数据。
'你好'.codePointAt(0)