本章 api 较多,为了更友好的代码提示,建议安装 @types/node 插件。
npm i @types/node -g
Buffer 是什么
缓冲区,它用来表示固定长度的 16 进制地址值的序列,JS 底层存储的都是二进制数据,二进制中一个字节,需要 8 个位,最大是 11111111,而 16 进制表示一个字节, 则只需要 2 位, 最大是 ff,表示起来简短许多。
// 二进制的 11111111 转十进制 => 255
// 十进制的 255 转 16 进制 => ff, 所以 16 进制表示字节,只需要 2 位即可。
// 这也是 buffer 的结果都是 2 位的原因,比如 <Buffer 0e cd 00>
255..toString(16); // ff
Buffer 表示的是固定长度的序列,就是说,一旦声明好,就不能随意扩容,如果想去修改 buffer 大小,改小可以截取内存,改大的话需要创造一个大的内存空间,将数据拷贝过去。
node 中如果使用二进制来表示的话,因为 node 中文都是 utf8 编码,一个汉字占用三个字节,每个字节又有8个位,那么表示一个汉字就需要 24 个二进制位,这样从文件中读取出来的数据就会异常的长。
需要注意的是,node 中单个字节就能表示的(英文,数字,符号等)使用的是 ascii 编码,需要多个字节的使用的是 uft8 编码,node 中所谓的数据转 16 进制,实际上转的只是对应编码映射表上的 key (10进制),具体还要转 10 进制,再去对应映射表上查找。
而浏览器中直接展示的就是十进制的数字,而不需要去 ascii 表上查找,因为浏览器最初是不能读写文件的,所以浏览器底层实现就规定了页面上展示的数字就是十进制的,而文字则是 utf8 编码 (charset=utf-8)
注意,编码和进制不是一个东西,编码可以理解成映射表,页面上显示给用户看到的是映射后的值,而进制数是用来描述这个数据在编码映射表中的位置的,所以进制数其实都是数字。
var bf = Buffer.from('1'); // 将 1 转 1 6进制
console.log(bf); // <Buffer 31>
// 16 进制 31 转 10 进制
3 * 16 + 1 * 1 = 49;
// 还需要去 ascii 映射表上查找 49,才能得到 1
Buffer 常用方法
new Buffer 创建 buffer(已弃用)
老的用法 (因为一些安全性问题,已经被弃用):
let bf = new Buffer('帅');
// <Buffer e5 b8 85>
// DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead.
Buffer.alloc 接收 size, 创建 buffer
// alloc 传入数字,代表创建几个字节大小的 Buffer 空间,十六进制值默认填最小值 00
let bf = Buffer.alloc(3); // <Buffer 00 00 00>
let bf2 =Buffer.alloc(1024); // 1024个字节 1kb
Buffer.from 接收数组,创建 buffer
接收一个由 16 进制地址组成的数组。
// 很少使用数组来定义 buffer,因为需要指定存放的内容
Buffer.from([0xff, 0xff]); // <Buffer ff ff>
// 我们知道,二进制一个字节最大数组是八个 1,也就代表 255,转成 16 进制为 ff
// 255..toString(16)
// 也就是说,16 进制一个字节表示最大数为 255
// 超过 255 则取余 (0 - 255 共 256 位, 对 256 取余所以 256 代表 1)
Buffer.from([257, 0xff,0xff]); // <Buffer 01 ff ff>
Buffer.from([-1, 0xff,0xff]); // <Buffer ff ff ff>
// 数组内必须是 16 进制的值(0x开头,0-9,a-f),或者 10 进制的数字
Buffer.from(['a', 0xff,0xff]); // <Buffer 00 ff ff> "a" 没有解析出来
Buffer.from([2, 0xff,0xff]); // <Buffer 00 ff ff>
Buffer.from 接收字符串,创建 buffer(主要用法)
Buffer 的主要用法,返回一个传入字符串转成的 16进制值序列的缓冲区
Buffer.from('杨帅'); // <Buffer e6 9d a8 e5 b8 85> 两个汉字 6个字节
Buffer.isBuffer
判断是不是一个 Buffer
let bf = Buffer.from('杨帅');
console.log(Buffer.isBuffer(bf)); // true
console.log(Buffer.isBuffer('a')); // falses
Buffer.toString
16 进制 buffer 转字符串,默认 utf8 编码,一般我们常用的就是 utf8 和 base64,但是不支持 gbk 哦。
let bf = Buffer.from('杨帅');
bf.toString(); // 杨帅
bf.toString('base64'); // 5p2o5biF
Buffer 下标访问
浏览器的特性,展示出来的都是 10 进制,所以下标访问会转 10 进制。
let bf = Buffer.from('杨帅');
console.log(bf); // <Buffer e6 9d a8 e5 b8 85>
console.log(bf[bf.length - 1]); // 133 打印出来会转 10 进制 8 * 16**1 + 5 = 133
Buffer.length
需要特别注意的一点是,Buffer 的长度指的是字节长度,不是字符串的长度哦
const bf1 = Buffer.from('哈哈');
const bf2 = Buffer.from('哎嘿');
console.log(bf1.length, bf2.length); // 6 6
// 想拿到源字符串的长度需要 toString
console.log(bf1.toString().length }); // 2
Buffer.slice
类似数组截取,不同的是,buffer 中存的是内存地址,所有截取后修改,二者同时改变。
const bf = Buffer.from([1, 2, 3, 4]);
const bf2 = bf.slice(0, 1);
console.log(bf2); // <Buffer 01>
bf2[0] = 100;
console.log(bf); // <Buffer 64 02 03 04>
Buffer.copy
const bf1 = Buffer.from('哈哈');
const bf2 = Buffer.from('哎嘿');
console.log(bf1.length, bf2.length);
const bigBf = Buffer.alloc(12); // 12 个字节的 buff
// 把 buf1 拷贝到 bigBf 上
// 要拷贝到的目标 buffer,目标 buffer 的开始,buf1 的开始拷贝的下标,结束拷贝的下标
bf1.copy(bigBf, 0, 0, 6);
bf2.copy(bigBf, 6, 0, 3);
console.log(bigBf.toString()); // 哈哈哎
Buffer.concat 合并
const bf1 = Buffer.from('哈哈');
const bf2 = Buffer.from('哎嘿');
// 这个 9 代表拼接后的结果只有 9 个字节,也就是只拼三个字
console.log(Buffer.concat([bf1, bf2], 9).toString()); // 哈哈哎
Buffer 的应用场景(文件操作)
// note.md
123456789
// buffer.js
const fs = require('fs');
const path = require('path');
// 不给编码,默认读取出来的都是 buffer(16进制)
let r = fs.readFileSync(path.resolve(__dirname, './note.md'));
console.log(r); // <Buffer 31 32 33 34 35 36 37 38 39>
有同学可能会疑问,为什么我的 10 进制的 1 读取出来是 16 进制的 31,16 进制的 31 转 10 进制却是 49 呢
3 * 16**1 + 1 * 16**0 = 49
这是因为 node 中数字的编码方式是 ascii,ascii 表上 49 表示的就是 1
浏览器的特性,展示出来的都是 10 进制,所以下标访问会转 10 进制:
console.log(r[0]); // 49
手动实现 copy 方法
/**
* @description 手动实现 Buffer copy 方法
* @param {*} target 要拷贝到的目标 buffer
* @param {*} targetStart 目标 buffer 写入下标
* @param {*} sourceStart 源 buffer 的开始下标 不写默认 0
* @param {*} sourceEnd 源 buffer 的结束下标 不写默认到最后
*/
Buffer.prototype.copy = function(target, targetStart, sourceStart = 0, sourceEnd = this.length) {
for (let i = sourceStart; i < sourceEnd; i++) {
target[targetStart++] = this[i];
}
}
const bf1 = Buffer.from('哈哈');
const bf2 = Buffer.from('哎嘿');
const bigBf = Buffer.alloc(12); // 12 个字节的 buff
bf1.copy(bigBf, 0, 0, 6);
bf2.copy(bigBf, 6, 3, 6);
console.log(bigBf.toString());
手动实现 concat 方法
/**
* @description 手动实现 Buffer concat 静态方法
* @param {*} bufferList
* @param {*} len
* @returns
*/
Buffer.concat = function(bufferList, len) {
if (len == undefined) {
// 如果没传 len 默认全部合并
len = bufferList.reduce((a, b) => a + b.length, 0);
}
let bigBuffer = Buffer.alloc(len);
let offset = 0;
for (let i = 0; i < bufferList.length; i++) {
let buf = bufferList[i];
// 如果当前项是 buffer,再合并进去
if (Buffer.isBuffer(buf)) {
buf.copy(bigBuffer, offset);
offset += buf.length;
}
}
return bigBuffer;
}
const bf1 = Buffer.from('哈哈');
const bf2 = Buffer.from('哎嘿');
console.log(Buffer.concat(['a', bf1, bf2], 9).toString()); // 哈哈哎嘿