二进制与编码
进制转换
// 十进制 -> 二进制
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)
HTML实体字符(character Entities)
<span>​</span>
<span>​</span> <!-- unicode的十进制表示-->
JavaScript字符表示
// 零宽字符 zeroWidthSpace
const str = '\u200B' // unicode的十六进制表示
TextEncoder
浏览器环境下通过TextEncoder
,可以将字符串通过utf-8
的格式编码成二进制(返回的是Typed Array,我们可以拿到对应的buffer)
const arr = new TextEncoder().encode('akara') // Uint8Array(5) [97, 107, 97, 114, 97, buffer: ArrayBuffer(5)]
const buffer = arr.buffer // ArrayBuffer(5)
[...new TextEncoder().encode('你')].map(i => i.toString(16)) // utf-8编码为 ['e4', 'bd', 'a0']
'你'.charCodeAt().toString(16) // utf-16编码为 4f60
TextDecoder
const arr = new Uint8Array([97, 107, 97, 114, 97])
new TextDecoder().decode(arr) // akara
encodeURI
HTTP请求报文头部的内容会经过ASCII编码后传输,这意味着我们无法在请求路径或参数上附带汉字等字符。而浏览器提供了encodeURI
和encodeURIComponent
方法来实现对这种复杂字符的编码,它会返回汉字以utf-8
编码后的二进制数值的字符表示。
encodeURI('你好') // '%E4%BD%A0%E5%A5%BD'
[...new TextEncoder().encode("你好")].map(item => item.toString(16)) // ['e4', 'bd', 'a0', 'e5', 'a5', 'bd']
const buffer = Buffer.from('你好') // Nodejs <Buffer e4 bd a0 e5 a5 bd>
decodeURI
decodeURI('%E4%BD%A0%E5%A5%BD') // 你好
相较于encodeURI
,encodeURIComponent
可以对更多的URL字符进行编码
encodeURI('http://baidu.com') // 'http://baidu.com'
encodeURIComponent('http://baidu.com') // 'http%3A%2F%2Fbaidu.com'
Base64
上述的内容都是单个字符的编码格式,而Base64
的作用是将任意二进制字节序转化为可见的字符。比如可以把图片的二进制数据编码成字符串进行存储等。
给定任意的字节序列,我们将其每三个字节共24位比特作为一组,然后把这24位比特又划分为4个6比特的小组,在每个小组的最高位添加00
的两个比特。此时,原本三个字节的数据变成了四个字节,每个新的字节有效位数为6位,我们可以把每个字节都根据ASCII
的规则映射为64种(即2^6
)不同的字符,因此这种编码方式被称为Base64
。
如上文所介绍,Base64
是一种二进制的编码方式,但在实际生活中我们会经常见到使用Base64
来编码字符串来进行混淆(伪加密)。比如使用window.btoa
可以将字符串转化为Base64
形式的字符,首先我们的参数必须是binary string(即每个字符解码后都只占一个字节),window.btoa
获取到字节序后再通过Base64
编码成ASCII字符。
Base64
的一个常用途径是对小型图片进行编码,如通过<img src="data:img/gif;base64,xxxxx" />
形式进行图片的加载来减少不必要的网络请求,但因为Base64
会使得编码后的数据比原先大三分之一,因此通常不会对大图进行编码。
fetch('data:image/png;base64,xxxxx').then(res => res.arrayBuffer())
window.btoa()
如上小节所介绍,通过浏览器提供的window.btoa
可以实现base64
的编码。
window.btoa('hello akara') // aGVsbG8gYWthcmE=
并且由于中文字符编码后都大于一个字节,所以window.btoa
无法直接解析中文参数,但可以借助encodeURIComponent()
来实现对应的效果
window.btoa(encodeURIComponent('你好')) // JUU0JUJEJUEwJUU1JUE1JUJE
window.atob()
window.atob('aGVsbG8gYWthcmE=') // hello akara
decodeURIComponent(window.atob('JUU0JUJEJUEwJUU1JUE1JUJE')) // 你好
Blob
Blob
只存在于浏览器环境中,可以大致把它视为一个类文件对象,是后续将介绍的File
对象的父类。
fetch()
.then(res => res.blob())
.then(blob => {
console.log(blob)
})
new Blob()
可用于根据ArrayBuffer
或字符串生成Blob
。
new Blob([JSON.stringify({
name: 'akara'
})], { type: 'application/json' })
new Blob([new Uint8Array([10, 20]).buffer]) // 需要注意第一个参数是数组包了一层
blob.arrayBuffer()
将Blob
转化为ArrayBuffer
,需要注意函数的返回值为Promise
blob.text()
将Blob
转化为字符串,编码为utf-8
,需要注意函数的返回值为Promise
blob.text().then(text => console.log(text))
blob.slice()
将文件或blob
分割成多个blob
,常用于大型图片的上传。
URL.createObjectURL(blob)
当后端将读取的文件作为响应体发送给前端时,我们常见的需求有:①下载文件到本地。②在本地显示图片。
这种需求的关键在于根据给定的文件或blob
生成一个URL路径,将其放入a
或img
标签的属性中。
const url = URL.createObjectURL(blob)
a.href = url // 结构类似于 blob:http://localhost:3000/486ef892-d4fc-485f-b4ab-fae272d35e55
a.download = '下载文件.txt' // 文件名
const url2 = URL.createObjectURL(blob)
img.src = url2
File
File
是Blob
的子类,相较于Blob
多了name
、lastModified
和lastModifiedDate
属性。通常可以从input
元素中获取到。
const el = document.querySelector('input')
console.log(el.files) // FileList
el.files[0] // File
FormData
通常当我们需要给后端发送文件时,要么直接把单个文件内容作为请求体,要么通过formData
来进行数据传输。
// 错误
fetch(url, {
data: {
file: new File(), // 会被JSON.stringify序列化
name: 'akara',
}
})
// 正确
const formData = new FormData();
formData.append('file', new File());
formData.append('name', 'akara')
fetch(url, {
data: formData
})
总结
通过上述几节我们了解到ArrayBuffer
、String
、Blob
的基本情况,并且知道它们直接是可以进行闭环任意转化的:
String -> Uint8Array(TextEncoder)
Uint8Array -> String(TextDecoder)
Uint8Array -> ArrayBuffer(.buffer)
ArrayBuffer -> Uint8Array(new Uint8Array)
ArrayBuffer -> Blob(new Blob)
Blob -> ArrayBuffer(arrayBuffer)
Blob -> String(text)
String -> Blob(new Blob)
FileReader
FileReader
用于读取文件中的数据,Blob
内置了一些方法来获取对应的ArrayBuffer
、字符串,而FileReader
也提供了类似的功能。
const reader = new FileReader()
reader.onload = function() {
console.log(reader.result)
}
reader.readAsXXX(blob)
readAsArrayBuffer()
功能类似于blob.arrayBuffer()
readAsText()
功能类似于blob.text()
readAsDataURL()
功能类似于URL.createObjectURL
,但稍微有些不同。
这个方式是将文件内容的字节序列通过Base64编码得到字符串,即以data:application/octet-stream;base64,
开头的长URL;而URL.createObjectURL()
实际上拿到的是一个以blob:http://xxx.com/xxx
开头的短URL,这个URL将会指向内存中的对应地址。
readAsBinaryString()
将二进制编码为JS字符串,因此是使用的UTF-16编码。
const blob = new Blob([new Uint8Array([0, 255, 0, 255, 100, 200])])
const reader = new FileReader();
reader.onload = function () {
console.log(Uint8Array.from(reader.result, char => char.charCodeAt(0))) // Uint8Array([0, 255, 0, 255, 100, 200])
// console.log(reader.result) 会显示乱码 ÿÿdÈ
}
reader.readAsBinaryString(blob)
stream
流对象用来处理Node中的流式数据,Stream
继承于eventEmitter
,因此实例存在on
方法,除此之外流对象的实例存在pipe
方法来实现流式数据的传输。
流对象有四种类型:
Writable
,比如fs.createWriteStream
、process.stdout
、res
Readable
,比如fs.createReadStream
、process.stdin
、req
Duplex
,可以当成Writable
和Readable
的结合Transform
Writable
对于Writable
类型的流对象,实例存在write()
和end()
方法。
Readable
对于Readable
类型的流对象,实例存在on('data')
和on('end')
方法。
pipeline
const { stdout, stdin } = require('process')
const { pipeline } = require('stream')
// 写法一
stdin.on('data', chunk => {
stdout.write(chunk)
})
// 写法二
stdin.pipe(stdout)
// 写法三
pipeline(stdin, stdout)
zlib
Node的Zlib
模块提供了基于Stream
的API来实现gzip
等格式的压缩或解压缩。
const zlib = require('zlib')
const { pipeline } = require('stream')
const gzip = zlib.createGzip() // 创建一个流对象
const source = fs.createReadStream('./a.txt')
const target = fs.createWriteStream('./a.txt.gz')
pipeline(source, gzip, target, err => console.log(err)) // 生成压缩文件a.txt.gz
当然我们也可以使用基于回调函数的写法来直接对文件进行压缩,经过测试可以把160Kb的页面压缩至4Kb,当浏览器识别到响应头部的Content-Encoding: gzip
,浏览器就会自动对响应的内容进行解压缩。
// server.js
const { promisify } = require('util')
const fs = require('fs')
const zlib = require('zlib')
const readFile = promisify(fs.readFile)
const gzip = promisify(zlib.gzip)
const http = require('http')
http.createServer(async (req, res) => {
if (req.url === '/') {
const page = await readFile('./index.html')
const data = await gzip(page)
res.setHeader('Content-Encoding', 'gzip')
res.end(data)
}
else if (req.url === '/test') {
let obj = {}
for (let i = 0; i < 10000; i++) {
obj[i] = i
}
const data = await gzip(JSON.stringify(obj))
res.setHeader('Content-Encoding', 'gzip')
res.end(data)
}
}).listen(3000)
// client.js
fetch('/test')
.then(res => res.json())
.then(data => console.log(data)) // 拿到obj