在 ES6 引入 TypedArray 以前,JavaScript 语言没有读取或操做二进制数据流的机制。 Buffer 类被引入做为 NodeJS API 的一部分,使其能够在 TCP 流或文件系统操做等场景中处理二进制数据流。
Buffer 属于 Global 对象,使用时不需引入,且 Buffer 的大小在建立时肯定,没法调整。npm
在 NodeJS v6.0.0
版本以前,Buffer 实例是经过 Buffer 构造函数建立的,即便用 new
关键字建立,它根据提供的参数返回不一样的 Buffer,但在以后的版本中这种声明方式就被废弃了,替代 new
的建立方式主要有如下几种。数组
用 Buffer.alloc
和 Buffer.allocUnsafe
建立 Buffer 的传参方式相同,参数为建立 Buffer 的长度,数值类型。缓存
1
2
3
4
5
6
7
8
复制代码 |
// Buffer.alloc 建立 Buffer
let buf1 = Buffer.alloc(6);
// Buffer.allocUnsafe 建立 Buffer
let buf2 = Buffer.allocUnsafe(6);
console.log(buf1); // <Buffer 00 00 00 00 00 00>
console.log(buf2); // <Buffer 00 e7 8f a0 00 00>
复制代码 |
经过代码能够看出,用 Buffer.alloc
和 Buffer.allocUnsafe
建立 Buffer 是有区别的,Buffer.alloc
建立的 Buffer 是被初始化过的,即 Buffer 的每一项都用 00
填充,而 Buffer.allocUnsafe
建立的 Buffer 并无通过初始化,在内存中只要有闲置的 Buffer 就直接 “抓过来” 使用。安全
Buffer.allocUnsafe
建立 Buffer 使得内存的分配很是快,但已分配的内存段可能包含潜在的敏感数据,有明显性能优点的同时又是不安全的,因此使用需格外 “当心”。bash
Buffer.from 支持三种传参方式:编辑器
ASCII
、UTF-8
、Base64
等等。传入字符串和字符编码:函数
1
2
3
复制代码 |
let buf = Buffer.from("hello", "utf8");
console.log(buf); // <Buffer 68 65 6c 6c 6f>
复制代码 |
传入数组:性能
1
2
3
复制代码 |
let buf = Buffer.from([1, 2, 3]);
console.log(buf); // <Buffer 01 02 03>
复制代码 |
1
2
3
4
复制代码 |
let buf = Buffer.from([0xe4, 0xbd, 0xa0, 0xe5, 0xa5, 0xbd]);
console.log(buf); // <Buffer e4 bd a0 e5 a5 bd>
console.log(buf.toString("utf8")); // 你好
复制代码 |
在 NodeJS 中不支持 GB2312
编码,默认支持 UTF-8
,在 GB2312
中,一个汉字占两个字节,而在 UTF-8
中,一个汉字占三个字节,因此上面 “你好” 的 Buffer 为 6
个十六进制数组成。ui
1
2
3
复制代码 |
let buf = Buffer.from(["1", "2", "3"]);
console.log(buf); // <Buffer 01 02 03>
复制代码 |
传入的数组成员能够是任何进制的数值,当成员为字符串的时候,若是值是数字会被自动识别成数值类型,若是值不是数字或成员为是其余非数值类型的数据,该成员会被初始化为 00
。this
建立的 Buffer 能够经过 toString
方法直接指定编码进行转换,默认编码为 UTF-8
。
传入 Buffer:
1
2
3
4
5
6
7
8
复制代码 |
let buf1 = Buffer.from("hello", "utf8");
let buf2 = Buffer.from(buf1);
console.log(buf1); // <Buffer 68 65 6c 6c 6f>
console.log(buf2); // <Buffer 68 65 6c 6c 6f>
console.log(buf1 === buf2); // true
console.log(buf1[0] === buf2[0]); // false
复制代码 |
当传入的参数为一个 Buffer 的时候,会建立一个新的 Buffer 并复制上面的每个成员。
Buffer 为引用类型,一个 Buffer 复制了另外一个 Buffer 的成员,当其中一个 Buffer 复制的成员有更改,另外一个 Buffer 对应的成员会跟着改变,由于指向同一个引用,相似于 “二维数组”。
1
2
3
4
5
复制代码 |
let arr1 = [1, 2, [3]];
let arr2 = arr1.slice();
arr2[2][0] = 5;
console.log(arr1); // [1, 2, [5]]
复制代码 |
Buffer 的 fill
方法能够向一个 Buffer 中填充数据,支持传入三个参数:
0
;1
2
3
4
复制代码 |
let buf = Buffer.alloc(3);
buf.fill(1);
console.log(buf); // <Buffer 01 01 01>
复制代码 |
1
2
3
4
复制代码 |
let buf = Buffer.alloc(6);
buf.fill(1, 2, 4);
console.log(buf); // <Buffer 00 00 01 01 00 00>
复制代码 |
上面代码能够看出填充数据是 “包前不包后的”,fill
的第一个参数也支持是多个字节,从被填充 Buffer 的起始位置开始,一直到结束,会循环填充这些字节,剩余的位置不够填充这几个字节,会填到哪算哪,有可能不完整,若是 fill
指定的结束位置大于了 Buffer 的长度,会抛出 RangeError
的异常。
1
2
3
4
复制代码 |
let buf = Buffer.alloc(6);
buf.fill("abc", 1, 5);
console.log(buf); // <Buffer 00 61 62 63 61 00>
复制代码 |
1
2
3
4
复制代码 |
let buf = Buffer.alloc(3);
buf.fill("abc", 4, 8);
console.log(buf); // throw new errors.RangeError('ERR_INDEX_OUT_OF_RANGE');
复制代码 |
Buffer 的 slice
方法与数组的 slice
方法用法彻底相同,相信数组的 slice
已经足够熟悉了,这里就很少赘述了,Buffer 中截取出来的都是 Buffer。
1
2
3
4
5
6
7
8
9
复制代码 |
let buf = Buffer.from("hello", "utf8");
let a = buf.slice(0, 2);
let b = buf.slice(2);
let b = buf.slice(-2);
console.log(a.toString()); // he
console.log(b.toString()); // llo
console.log(c.toString()); // o
复制代码 |
Buffer 的 indexOf
用法与数组和字符串的 indexOf
相似,第一个参数为查找的项,第二个参数为查找的起始位置,不一样的是,对于 Buffer 而言,查找的多是一个字符串,表明多个字节,查找的字节在 Buffer 中必须有连续相同的字节,返回连续的字节中第一个字节的索引,没查找到返回 -1
。
1
2
3
4
5
复制代码 |
let buf = Buffer.from("你*好*吗", "utf8");
console.log(buf); // <Buffer e4 bd a0 2a e5 a5 bd 2a e5 90 97>
console.log(buf.indexOf("*")); // 3
console.log(buf.indexOf("*", 4)); // 7
复制代码 |
Buffer 的 copy 方法用于将一个 Buffer 的字节复制到另外一个 Buffer 中去,有四个参数:
1
2
3
4
5
6
7
复制代码 |
let targetBuf = Buffer.alloc(6);
let sourceBuf = Buffer.from("你好", "utf8");
// 将 “你好” 复制到 targetBuf 中
sourceBuf.copy(targetBuf, 0, 0, 6);
console.log(targetBuf.toString()); // 你好
复制代码 |
1
2
3
4
5
复制代码 |
let targetBuf = Buffer.alloc(3);
let sourceBuf = Buffer.from("你好", "utf8");
sourceBuf.copy(targetBuf, 0, 0, 6);
console.log(targetBuf.toString()); // 你
复制代码 |
上面第二个案例中虽然要把整个源 Buffer 都复制进目标 Buffer 中,可是因为目标 Buffer 的长度只有 3
,因此最终只能复制进去一个 “你” 字。
Buffer 与数组不一样,不能经过操做 length
和索引改变 Buffer 的长度,Buffer 一旦被建立,长度将保持不变。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码 |
// 数组
let arr = [1, 2, 3];
arr[3] = 4;
console.log(arr); // [1, 2, 3, 4]
arr.length = 5;
console.log(arr); // [1, 2, 3, 4, empty]
// Buffer
let buf = Buffer.alloc(3);
buf[3] = 0x00;
console.log(buf); // <Buffer 00 00 00>
buf.length = 5;
console.log(buf); // <Buffer 00 00 00>
console.log(buf.length); // 3
复制代码 |
经过上面代码能够看出数组能够经过 length
和索引对数组的长度进行改变,可是 Buffer 中相似的操做都是不生效的。
copy
方法的 Polyfill:
1
2
3
4
5
复制代码 |
Buffer.prototype.myCopy = function (target, targetStart, sourceStart, sourceEnd) {
for(let i = 0; i < sourceEnd - sourceStart; i++) {
target[targetStart + i] = this[sourceStart + i];
}
}
复制代码 |
与数组相似,Buffer 也存在用于拼接多个 Buffer 的方法 concat
,不一样的是 Buffer 中的 concat
不是实例方法,而是静态方法,经过 Buffer.concat
调用,且传入的参数不一样。
Buffer.concat
有两个参数,返回值是一个新的 Buffer:
Buffer.concat
会将数组中的 Buffer 进行拼接,存入新 Buffer 并返回,若是传入第二个参数规定了返回 Buffer 的长度,那么返回值存储拼接后前规定长度个字节。
1
2
3
4
5
6
7
8
9
10
11
复制代码 |
let buf1 = Buffer.from("你", "utf8");
let buf2 = Buffer.from("好", "utf8");
let result1 = Buffer.concat([buf1, buf2]);
let result2 = Buffer.concat([buf1, buf2], 3);
console.log(result1); // <Buffer e4 bd a0 e5 a5 bd>
console.log(result1.toString()); // 你好
console.log(result2); // <Buffer e4 bd a0>
console.log(result2.toString()); // 你
复制代码 |
Buffer.concat
方法的 Polyfill:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码 |
Buffer.myConcat = function (bufferList, len) {
// 新 Buffer 的长度
len = len || bufferList.reduce((prev, next) => prev + next.length, 0);
let newBuf = Buffer.alloc(len); // 建立新 Buffer
let index = 0; // 下次开始的索引
// 循环存储 Buffer 的数组进行复制
bufferList.forEach(buf => {
buf.myCopy(newBuf, index, 0, buf.length);
index += buf.length;
});
return newBuf;
}
复制代码 |
Buffer.isBuffer
是用来判断一个对象是不是一个 Buffer,返回布尔值。
1
2
3
4
5
复制代码 |
let obj = {};
let buf = Buffer.alloc(6);
console.log(Buffer.isBuffer(obj)); // false
console.log(Buffer.isBuffer(buf)); // true
复制代码 |
字符串中的 split
是常用的方法,能够用分隔符将字符串切成几部分存储在数组中,Buffer 自己没有 split
方法,可是也会有相似的使用场景,因此咱们在 Buffer 中本身封装一个 split
。
Buffer 的 split
方法参数为一个分隔符,这个分隔符多是一个或多个字节的内容,返回值为一个数组,分隔开的部分做为独立的 Buffer 存储在返回的数组中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码 |
Buffer.prototype.split = function (sep) {
let len = Buffer.from(sep).length; // 分隔符所占的字节数
let result = []; // 返回的数组
let start = 0; // 查找 Buffer 的起始位置
let offset = 0; // 偏移量
// 循环查找分隔符
while ((offset = this.indexOf(sep, start)) !== -1) {
// 将分隔符以前的部分截取出来存入
result.push(this.slice(start, offset));
start = offset + len;
}
// 处理剩下的部分
return result.push(this.slice(start));
}
复制代码 |
验证 split
方法:
1
2
3
4
5
6
7
8
9
10
复制代码 |
let buf = Buffer.from("哈登爱篮球爱夜店", "utf8");
let bufs = buf.split("爱");
console.log(bufs);
// [ <Buffer e5 93 88 e7 99 bb>,
// <Buffer e7 af ae e7 90 83>,
// <Buffer e5 a4 9c e5 ba 97> ]
newBufs = bufs.map(buf => buf.toString());
console.log(newBufs); // [ '哈登', '篮球', '夜店' ]
复制代码 |
咱们知道 NodeJS 中的默认编码为 UTF-8
,且不支持 GB2312
编码,假如如今有一个编码格式为 GB2312
的 txt
文件,内容为 “你好”,如今咱们使用 NodeJS 去读取它,因为在 UTF-8
与 GB2312
编码中汉字所占字节数不一样,因此读出的内容没法解析,即为乱码。
1
2
3
4
5
6
7
8
9
10
11
复制代码 |
// 引入依赖
const fs = require("fs");
const path = require("path");
let buf = Buffer.from("你好", "utf8");
let result = fs.readFileSync(path.resolve(__dirname, "a.txt"));
console.log(buf); // <Buffer e4 bd a0 e5 a5 bd>
console.log(buf.toString()); // 你好
console.log(result); // <Buffer c4 e3 ba c3>
console.log(result.toString()); // ���
复制代码 |
若是必定要在 NodeJS 中来正确解析这样的内容,这样的问题仍是有办法解决的,咱们须要借助 iconv-lite
模块,这个模块能够将一个 Buffer 按照指定的编码格式进行编码或解码。
因为 iconv-lite
是第三方提供的模块,在使用前须要安装,安装命令以下:
npm install iconv-lite
若是想正确的读出其余编码格式文件的内容,上面代码应该更改成:
1
2
3
4
5
6
7
8
复制代码 |
// 引入依赖
const fs = require("fs");
const path = require("path");
const iconvLite = require("iconv-lite");
let result = fs.readFileSync(path.resolve(__dirname, "a.txt"));
console.log(iconvLite.decode(result, "gb2312")); // 你好
复制代码 |
上面读取 GB2312
编码的 txt
文件也能够经过打开文件从新保存为 UTF-8
或用编辑器直接将编码手动修改成 UTF-8
,此时读取的文件不须要进行编码转换,可是会产生新的问题。
1
2
3
4
5
6
7
8
9
复制代码 |
// 引入依赖
const fs = require("fs");
const path = require("path");
let buf = Buffer.from("你好", "utf8");
let result = fs.readFileSync(path.resolve(__dirname, "a.txt"));
console.log(buf); // <Buffer e4 bd a0 e5 a5 bd>
console.log(result); // <Buffer ef bb bf e4 bd a0 e5 a5 bd>
复制代码 |
在手动修改 txt
文件编码后执行上面代码,发现读取的 Buffer 与正常状况相比前面多出了三个字节,只要存在文件编码的修改就会在这个文件的前面产生多余的字节,叫作 BOM
头。
BOM
头是用来判断文本文件是哪种 Unicode
编码的标记,其自己是一个 Unicode
字符,位于文本文件头部。
虽然 BOM
头起到了标记文件编码的做用,可是它并不属于文件的内容部分,所以会产生一些问题,如文件编码发生变化后没法正确读取文件的内容,或者多个文件在合并的过程当中,中间会夹杂着这些多余内容,因此在 NodeJS 文件操做的源码中,Buffer 编码转换的模块 iconv-lite
中,以及 Webpack 对项目文件进行打包编译时都进行了去掉 BOM
头的操做。
为了让上面的代码能够正确的读取并解析编码被手动修改过的文件内容,咱们这里也须要进行去掉 BOM
头的操做。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码 |
function BOMStrip(result) {
if (Buffer.isBuffer(result)) {
// 若是读取的内容为 Buffer
if (result[0] === 0xef && result[1] === 0xbb && result[2] === 0xbf) {
// 若前三个字节是否和 BOM 头的前三字节相同,去掉 BOM 头
return Buffer.slice(3);
}
} else {
// 若是不是 Buffer
if (result.charCodeAt(0) === 0xfeff) {
// 判断第一项是否和 BOM 头的十六进制相同,去掉 BOM 头
return result.slice(1);
}
}
}
复制代码 |
使用去掉 BOM
头的方法并验证上面读文件的案例:
1
2
3
4
5
6
7
8
9
10
复制代码 |
// 引入依赖
const fs = require("fs");
const path = require("path");
// 两种方式读文件
let result1 = fs.readFileSync(path.resolve(__dirname, "a.txt"));
let result2 = fs.readFileSync(path.resolve(__dirname, "a.txt"), "utf8");
console.log(BOMStrip(result1).toString()); // 你好
console.log(BOMStrip(result2)); // 你好
复制代码 |
1
2
3
4
5
6
7
复制代码 |
let buf = Buffer.from("你好", "utf8");
let a = buf.slice(0, 2);
let b = buf.slice(2, 6);
console.log(a.toString()); // �
console.log(b.toString()); // �好
复制代码 |
UTF-8
编码,一个汉字三个字节,使用 slice
方法对一个表达汉字的 Buffer 进行截取,若是截取长度不是 3
的整数倍,此时没法正确解析,会显示乱码,相似这种状况可使用模块 string_decoder
对不能组成汉字的 Buffer 进行缓存,string_decoder
是核心模块,不须要安装。
1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码 |
// 引入依赖
const { StringDecoder } = require("string_decoder");
let buf = Buffer.from("你好", "utf8");
let a = buf.slice(0, 2);
let b = buf.slice(2, 6);
// 建立 StringDecoder 实例
let sd = new StringDecoder();
console.log(sd.write(a));
console.log(sd.write(b)); // 你好
复制代码 |
上面代码中使用了 string_decoder
后,截取的 Buffer 不能组成一个汉字的时候不打印,进行缓存,等到能够正确解析时取出缓存,从新拼接后打印。