Buffer 概述

ES6 引入 TypedArray 之前,JavaScript 语言没有读取或操作二进制数据流的机制,Node.js 实现了 Buffer 相关的 API,使其可以在 TCP 流或文件系统操作等场景中处理二进制数据流,Buffer 属于 Global 对象,使用时不需引入,且 Buffer 的大小在创建时确定,无法调整。

创建 Buffer

Node.js v6.0.0 版本之前,Buffer 实例是通过 Buffer 构造函数创建的,即使用 new 关键字创建,它根据提供的参数返回不同的 Buffer,但在之后的版本中这种声明方式就被废弃了,替代 new 的创建方式主要有以下几种。

Buffer.alloc 和 Buffer.allocUnsafe

Buffer.allocBuffer.allocUnsafe 创建 Buffer 的传参方式相同,参数为创建 Buffer 的长度,数值类型。

/* Buffer.alloc 和 Buffer.allocUnsafe 创建 Buffer */
// Buffer.alloc 创建 Buffer
const buf1 = Buffer.alloc(6);

// Buffer.allocUnsafe 创建 Buffer
const 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.allocBuffer.allocUnsafe 创建 Buffer 是有区别的,Buffer.alloc 创建的 Buffer 是被初始化过的,Buffer 的每一项都用 00 填充,而 Buffer.allocUnsafe 创建的 Buffer 并没有经过初始化,在内存中只要有闲置的 Buffer 就直接 “抓过来” 使用。

Buffer.allocUnsafe 创建 Buffer 使得内存的分配非常快,但已分配的内存段可能包含潜在的敏感数据,有明显性能优势的同时又是不安全的,所以需谨慎使用。

Buffer.from

Buffer.from 支持三种传参方式:

  • 第一个参数为字符串,第二个参数为字符编码,如 ASCIIUTF-8Base64 等等;
  • 传入一个数组,数组的每一项会以十六进制存储为 Buffer 的每一项;
  • 传入一个 Buffer,会将 Buffer 的每一项作为新返回 Buffer 的每一项。

传入字符串和字符编码:

/* 传入字符串和字符编码 */
const buf = Buffer.from('hello', 'utf8');

console.log(buf); // <Buffer 68 65 6c 6c 6f>

传入数组:

/* 数组成员为十进制数 */
const buf = Buffer.from([1, 2, 3]);

console.log(buf); // <Buffer 01 02 03>
/* 数组成员为十六进制数 */
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')); // 你好

Node.js 中不支持 GB2312 编码,默认支持 UTF-8,在 GB2312 中,一个汉字占两个字节,而在 UTF-8 中,一个汉字占三个字节,所以上面 “你好” 的 Buffer6 个十六进制数组成。

/* 数组成员为字符串类型的数字 */
const buf = Buffer.from(['1', '2', '3']);

console.log(buf); // <Buffer 01 02 03>

传入的数组成员可以是任何进制的数值,当成员为字符串的时候,如果值是数字会被自动识别成数值类型,如果值不是数字或成员为是其他非数值类型的数据,该成员会被初始化为 00

创建的 Buffer 可以通过 toString 方法直接指定编码进行转换,默认编码为 UTF-8

传入 Buffer:

/* 传入一个 Buffer */
const buf1 = Buffer.from('hello', 'utf8');

const 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 对应的成员会跟着改变,因为指向同一个引用,类似于 “二维数组”。

/* Buffer 类比二维数组 */
const arr1 = [1, 2, [3]];
const arr2 = arr1.slice();

arr2[2][0] = 5;
console.log(arr1); // [1, 2, [5]]

Buffer 的常用方法

fill

Bufferfill 方法可以向一个 Buffer 中填充数据,支持传入三个参数:

  • value:将要填充的数据;
  • start:填充数据的开始位置,不指定默认为 0
  • end:填充数据的结束位置,不指定默认为 Buffer 的长度。
const buf = Buffer.alloc(3);

buf.fill(1);
console.log(buf); // <Buffer 01 01 01>
const buf = Buffer.alloc(6);

buf.fill(1, 2, 4);
console.log(buf); // <Buffer 00 00 01 01 00 00>

上面代码可以看出填充数据是 “包前不包后的”,fill 的第一个参数也支持是多个字节,从被填充 Buffer 的起始位置开始,一直到结束,会循环填充这些字节,剩余的位置不够填充这几个字节,会填到哪算哪,有可能不完整,如果 fill 指定的结束位置大于了 Buffer 的长度,会抛出 RangeError 的异常。

const buf = Buffer.alloc(6);

buf.fill('abc', 1, 5);
console.log(buf); // <Buffer 00 61 62 63 61 00>
const buf = Buffer.alloc(3);

buf.fill('abc', 4, 8);
console.log(buf); // throw new errors.RangeError('ERR_INDEX_OUT_OF_RANGE');

slice

Bufferslice 方法与数组的 slice 方法用法完全相同,相信数组的 slice 已经足够熟悉了,这里就不多赘述了,Buffer 中截取出来的都是 Buffer

const buf = Buffer.from('hello', 'utf8');

const a = buf.slice(0, 2);
const b = buf.slice(2);
const c = buf.slice(-2);

console.log(a.toString()); // he
console.log(b.toString()); // llo
console.log(c.toString()); // lo

indexOf

BufferindexOf 用法与数组和字符串的 indexOf 类似,第一个参数为查找的项,第二个参数为查找的起始位置,不同的是,对于 Buffer 而言,查找的可能是一个字符串,代表多个字节,查找的字节在 Buffer 中必须有连续相同的字节,返回连续的字节中第一个字节的索引,没查找到返回 -1

const 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
console.log(buf.indexOf('*', 4)); // 7

copy

Buffercopy 方法用于将一个 Buffer 的字节复制到另一个 Buffer 中去,有四个参数:

  • target:目标 Buffer
  • targetStart:目标 Buffer 的起始位置;
  • sourceStart:源 Buffer 的起始位置;
  • sourceEnd:源 Buffer 的结束位置。
/* 容器 Buffer 长度充足 */
const targetBuf = Buffer.alloc(6);
const sourceBuf = Buffer.from('你好', 'utf8');

// 将 “你好” 复制到 targetBuf 中
sourceBuf.copy(targetBuf, 0, 0, 6);

console.log(targetBuf.toString()); // 你好
/* 容器 Buffer 长度不足 */
const targetBuf = Buffer.alloc(3);
const sourceBuf = Buffer.from('你好', 'utf8');

sourceBuf.copy(targetBuf, 0, 0, 6);
console.log(targetBuf.toString()); // 你

上面第二个案例中虽然要把整个源 Buffer 都复制进目标 Buffer 中,但是由于目标 Buffer 的长度只有 3,所以最终只能复制进去一个 “你” 字。

Buffer 与数组不同,不能通过操作 length 和索引改变 Buffer 的长度,Buffer 一旦被创建,长度将保持不变。

/* 数组对比 Buffer —— 操作 length */
// 数组
const 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
const 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

/* 模拟 copy 方法 */
Buffer.prototype.myCopy = function (target, targetStart, sourceStart, sourceEnd) {
  for (let i = 0; i < sourceEnd - sourceStart; i++) {
    target[targetStart + i] = this[sourceStart + i];
  }
}

Buffer.concat

与数组类似,Buffer 也存在用于拼接多个 Buffer 的方法 concat,不同的是 Buffer 中的 concat 不是实例方法,而是静态方法,通过 Buffer.concat 调用,且传入的参数不同。

Buffer.concat 有两个参数,返回值是一个新的 Buffer

  • 第一个参数为一个数组,数组中的每一个成员都是一个 Buffer
  • 第二个参数代表新 Buffer 的长度,默认值为数组中每个 Buffer 长度的总和。

Buffer.concat 会将数组中的 Buffer 进行拼接,存入新 Buffer 并返回,如果传入第二个参数规定了返回 Buffer 的长度,那么返回值存储拼接到规定长度个字节。

const buf1 = Buffer.from('你', 'utf8');
const buf2 = Buffer.from('好', 'utf8');

const result1 = Buffer.concat([buf1, buf2]);
const 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

/* 模拟 Buffer.concat */
Buffer.myConcat = function (bufferList, len) {
  // 新 Buffer 的长度
  len = len || bufferList.reduce((prev, next) => prev + next.length, 0);

  let index = 0; // 下次开始的索引
  const newBuf = Buffer.alloc(len); // 创建新 Buffer

  // 循环存储 Buffer 的数组进行复制
  bufferList.forEach(buf => {
    buf.myCopy(newBuf, index, 0, buf.length);
    index += buf.length;
  });

  return newBuf;
}

Buffer.isBuffer

Buffer.isBuffer 是用来判断一个对象是否是一个 Buffer,返回布尔值。

const obj = {};
const buf = Buffer.alloc(6);

console.log(Buffer.isBuffer(obj)); // false
console.log(Buffer.isBuffer(buf)); // true

封装一个 split

字符串的 split 是经常使用的方法,可以用分隔符将字符串切成几部分存储在数组中,Buffer 本身没有 split 方法,但是也会有类似的使用场景,所以我们在 Buffer 上自己实现一个 split

功能:Buffersplit 方法参数为一个分隔符,这个分隔符可能是一个或多个字节的内容,返回值为一个数组,分隔开的部分作为独立的 Buffer 存储在返回的数组中。

/* 封装 Buffer 的 split 方法 */
Buffer.prototype.split = function (sep) {
  const len = Buffer.from(sep).length; // 分隔符所占的字节数
  const result = []; // 返回的数组
  let start = 0; // 查找 Buffer 的起始位置
  let offset = 0; // 偏移量

  // 循环查找分隔符
  while ((offset = this.indexOf(sep, start)) !== -1) {
    // 将分隔符之前的部分截取出来存入
    result.push(this.slice(start, offset));
    start = offset + len;
  }

  // 处理剩下的部分
  result.push(this.slice(start));

  // 返回结果
  return result;
}

验证 split 方法:

/* 验证 split */
const buf = Buffer.from('哈登爱篮球爱夜店', 'utf8');
const 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> ]

const newBufs = bufs.map(buf => buf.toString());
console.log(newBufs); // [ '哈登', '篮球', '夜店' ]

Buffer 的编码转换

我们知道 Node.js 中的默认编码为 UTF-8,且不支持 GB2312 编码,假如现在有一个编码格式为 GB2312txt 文件,内容为 “你好”,现在我们使用 Node.js 去读取它,由于在 UTF-8GB2312 编码中汉字所占字节数不同,所以读出的内容无法解析,即为乱码。

// 引入依赖
const fs = require('fs');
const path = require('path');

const buf = Buffer.from('你好', 'utf8');
const 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()); // ���

如果一定要在 Node.js 中来正确解析这样的内容,这样的问题还是有办法解决的,我们需要借助 iconv-lite 模块,这个模块可以将一个 Buffer 按照指定的编码格式进行编码或解码。

由于 iconv-lite 是第三方提供的模块,在使用前需要安装,安装命令如下:

$ npm install iconv-lite

如果想正确的读出其他编码格式文件的内容,上面代码应该更改为:

// 引入依赖
const fs = require('fs');
const path = require('path');
const iconvLite = require('iconv-lite');

const result = fs.readFileSync(path.resolve(__dirname, 'a.txt'));

console.log(iconvLite.decode(result, 'gb2312')); // 你好

去掉 BOM 头

上面读取 GB2312 编码的 txt 文件也可以通过打开文件重新保存为 UTF-8 或用编辑器直接将编码手动修改为 UTF-8,此时读取的文件不需要进行编码转换,但是会产生新的问题。

/* 产生 BOM 头 */
// 引入依赖
const fs = require('fs');
const path = require('path');

const buf = Buffer.from('你好', 'utf8');
const 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 头起到了标记文件编码的作用,但是它并不属于文件的内容部分,因此会产生一些问题,如文件编码发生变化后无法正确读取文件的内容,或者多个文件在合并的过程中,中间会夹杂着这些多余内容,所以在 Node.js 文件操作的源码中,Buffer 编码转换的模块 iconv-lite 中,以及 Webpack 对项目文件进行打包编译时都进行了去掉 BOM 头的操作。

为了让上面的代码可以正确的读取并解析编码被手动修改过的文件内容,我们这里也需要进行去掉 BOM 头的操作。

/* 去掉 BOM 头的方法 */
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 头的方法并验证上面读文件的案例:

/* 验证去 BOM 头的方法 */
// 引入依赖
const fs = require('fs');
const path = require('path');

// 两种方式读文件
const result1 = fs.readFileSync(path.resolve(__dirname, 'a.txt'));
const result2 = fs.readFileSync(path.resolve(__dirname, 'a.txt'), 'utf8');

console.log(BOMStrip(result1).toString()); // 你好
console.log(BOMStrip(result2)); // 你好

缓存 Buffer

/* 产生乱码问题 */
const buf = Buffer.from('你好', 'utf8');

const a = buf.slice(0, 2);
const b = buf.slice(2, 6);

console.log(a.toString()); // �
console.log(b.toString()); // �好

UTF-8 编码,一个汉字三个字节,使用 slice 方法对一个表达汉字的 Buffer 进行截取,如果截取长度不是 3 的整数倍,此时无法正确解析,会显示乱码,这种情况可以使用模块 string_decoder 对不能组成汉字的 Buffer 进行缓存,string_decoder 是核心模块,不需要安装。

/* 缓存 Buffer */
// 引入依赖
const { StringDecoder } = require('string_decoder');

const buf = Buffer.from('你好', 'utf8');

const a = buf.slice(0, 2);
const b = buf.slice(2, 6);

// 创建 StringDecoder 实例
const sd = new StringDecoder();

console.log(sd.write(a));
console.log(sd.write(b)); // 你好

上面代码中使用了 string_decoder 后,截取的 Buffer 不能组成一个汉字的时候不打印,进行缓存,等到可以正确解析时取出缓存,重新拼接后打印。