为什么需要专门的字节序列搜索
JavaScript原生处理文本,网络流和文件系统传递的却是字节。把字节转成字符串再搜索,多了一次解码开销,而且如果字节序列不是合法的目标编码,转换还会引入替换字符,搜索的已经不是原始数据了。Buffer.includes()跳过了这一步,直接在字节层面判断“这段数据里有没有我关心的字节序列”。它解决的问题很窄,但在这个窄范围内,它是正确且直接的。
这个方法返回布尔值,语义比返回索引的indexOf()更贴近“有没有”这个问题本身。
签名与参数的精确含义
buf.includes(value[, byteOffset][, encoding])
value接受字符串、Buffer、Uint8Array或整数。整数必须是0到255之间的有效字节值。
byteOffset是搜索起始位置。负数表示从缓冲区末尾向前计算。这是官方文档明确支持的行为,但容易被忽略。buf.includes('world',-5)从倒数第5个字节开始向后搜索。
encoding仅在value为字符串时生效,决定如何将字符串解码为字节序列再进行比较。默认utf8。
方法返回true或false。
官方文档对这一点的描述非常直接:Equivalenttobuf.indexOf()!==-1。理解这个等价关系很重要,因为indexOf()的行为文档更详细,遇到困惑时可以去查indexOf()的说明。
三种value类型的搜索行为
搜索单个字节:
const buf = Buffer.from('code-number');
// 97 是 'a' 的 ASCII 值
console.log(buf.includes(97)); // true
console.log(buf.includes(99)); // true,'c'
console.log(buf.includes(122)); // false,'z'
搜索字节序列:
const buf = Buffer.from('code-number-learn');
console.log(buf.includes(Buffer.from('number'))); // true
console.log(buf.includes(Buffer.from('python'))); // false
搜索字符串时会先按指定编码转为字节。这一步的语义与buf.indexOf(string)一致,编码错误或字符串包含目标编码无法表示的字符时,行为取决于解码器的处理方式,需要结合具体编码判断。
如果value是数字但带小数,会被强制转换为整数:99.9变成99,等价于搜索'c'。如果数值超过255,会取模256。这类强制转换在indexOf()的文档中有明确描述,includes()共享同一套底层比较逻辑。
空值与边界行为
空字符串和空Buffer的搜索行为值得单独说明。当value为空且byteOffset小于buf.length时,搜索立即返回true。当byteOffset等于或超过buf.length时,也返回true,因为空序列“存在于”任何位置,包括末尾。这个行为继承自indexOf()的规范。
byteOffset为负数时,从末尾计算偏移。-4在长度为10的缓冲区上等价于偏移6。如果计算后的偏移仍然为负,会被当作0,搜索整个缓冲区。
byteOffset传入非数字类型时,会尝试强制转换。undefined、{}、[]、null经强制转换后产生NaN或0,效果等同于从索引0开始搜索。这与String#indexOf()的行为一致,属于JavaScript类型强制的延续。
单元测试与边界验证
围绕includes()的测试需要覆盖正常命中、未命中、偏移生效、编码差异和空值场景。下面是一组基于assert的验证。
const assert = require('node:assert/strict');
function testIncludesBasic() {
const buf = Buffer.from('code-number');
assert.ok(buf.includes('code'));
assert.ok(buf.includes(Buffer.from('number')));
assert.ok(buf.includes(45)); // '-' 的 ASCII 码
}
function testIncludesNotFound() {
const buf = Buffer.from('code-number');
assert.strictEqual(buf.includes('python'), false);
assert.strictEqual(buf.includes(0), false); // NUL 字节不在其中
}
function testIncludesOffset() {
const buf = Buffer.from('code-number');
// 'code' 在索引 0,从索引 1 开始搜索不应该找到
assert.strictEqual(buf.includes('code', 1), false);
// 'number' 从索引 5 开始找得到
assert.ok(buf.includes('number', 5));
}
function testIncludesNegativeOffset() {
const buf = Buffer.from('code-number');
assert.ok(buf.includes('number', -6));
}
function testIncludesEmptyValue() {
const buf = Buffer.from('code');
assert.ok(buf.includes(''));
assert.ok(buf.includes(Buffer.alloc(0)));
assert.ok(buf.includes('', buf.length));
}
testIncludesOffset中的第一个断言容易写错:从索引1开始搜索'code',但'code'的完整字节序列在索引1处已经不存在了,只有'ode'。所以false是正确的结果。
与indexOf和lastIndexOf的职责对比
三个方法共享相同的搜索核心,区别在于返回值和方向。
includes()返回布尔值,用于判断“存在性”。代码意图清晰,读起来不需要额外的比较。
indexOf()返回首次出现的索引或-1。当你需要知道“在哪里”而不只是“有没有”时使用。buf.includes('is')和buf.indexOf('is')!==-1在结果上等价,但前者更直接。
lastIndexOf()从后向前搜索,返回之后一次出现的索引。它同样有字符串和字节序列的搜索能力。
选择标准很简单:只要存在性判断用includes();需要位置信息用indexOf();需要之后一个位置用lastIndexOf()。不要为了“省一个方法”而用indexOf()!==-1替代includes(),后者可读性更好,而且不存在把indexOf返回值误当成布尔值的风险。
与TypedArray生态的关系
Buffer继承自Uint8Array,但includes()是Buffer自己的方法,不是TypedArray标准接口的一部分。Uint8Array有includes(),但那是从Array.prototype继承来的,比较的是元素值,不涉及字符串编码,也不接受Buffer或字符串作为参数。
const u8 = new Uint8Array([1, 2, 3]);
console.log(u8.includes(2)); // true,标准 TypedArray 行为
// u8.includes('2') 会尝试将 '2' 转为数字 2,结果也是 true
// 但 u8.includes(Buffer.from('2')) 不会做字节序列匹配
这是容易混淆的地方。Buffer的includes()有额外的编码和字节序列语义,普通Uint8Array没有。在只接受Uint8Array的函数中,不要依赖Buffer特有的搜索行为。
常见误区速查
把includes()当成字符串搜索。buf.includes('abc')确实可以用于文本Buffer,但它的底层是字节比较。如果Buffer包含多字节字符,byteOffset的调整可能落在字符中间,产生看似不合理的匹配结果。文本搜索用toString()之后再搜字符串更可控,除非有明确的性能理由直接操作字节。
忽略byteOffset对空值的影响。buf.includes('',buf.length)返回true。如果业务逻辑依赖“空字符串只在缓冲区内部才算存在”,需要额外判断。
以为includes()支持正则。它不支持。搜索模式必须是固定字节序列。需要正则语义时先转字符串。
在循环中用includes()做重复搜索。每次调用都从头(或从byteOffset指定的位置)线性扫描。大缓冲区中反复调用includes()是O(n)乘以调用次数的开销。需要多次查找时,考虑手动实现或用更适合的算法。
性能考量
includes()的底层是线性扫描。对于几KB到几十KB的缓冲区,开销可以忽略。但缓冲区达到MB级别且搜索频率较高时,需要考虑。
搜索模式如果是重复出现的短序列,可以利用byteOffset从上次命中位置之后继续,避免重复扫描已经覆盖的区域。
如果搜索的是纯字符串模式且数据本身是文本,先做一次toString()再用String.prototype.includes()可能更快,因为V8对字符串搜索有优化,而且避免了每次调用时的编码转换。但前提是Buffer是合法的目标编码,且你接受字符串搜索的语义。
Buffer.includes()的优势在于不产生中间字符串,适合处理二进制协议帧、网络包负载这类内容。判断依据是“搜索目标是否是结构化的字节序列”,如果是,字节级搜索更准确。
版本差异
Buffer.includes()在Node.js6.0.0中引入。在此之前,判断Buffer包含关系的标准写法是buf.indexOf(value)!==-1。
参数签名在早期版本中为(value,byteOffset,encoding),与indexOf()和lastIndexOf()保持一致。后续版本没有对这个签名做破坏性调整。Node.js8.0.0之前newBuffer()的行为有变化,但那是Buffer构造方式的问题,不是includes()本身的变化。
Uint8Array被加入value可接受类型列表是后续版本的事。在早期文档中,value的类型标注通常是string|Buffer|integer,后来才扩展到包含Uint8Array。
异步场景中的使用
includes()是同步方法。在流数据处理中,如果需要在数据到达时检查某个标记字节序列,典型写法是在data事件的回调中操作当前到达的Buffer。
const { Transform } = require('node:stream');
const CRLF = Buffer.from('\r\n');
const frameSplitter = new Transform({
transform(chunk, encoding, callback) {
if (chunk.includes(CRLF)) {
this.push(chunk);
}
callback();
}
});
这里的问题是,网络分帧时\r\n可能跨越两个chunk。单个chunk的includes()找不到不代表数据流中不存在。跨chunk的搜索需要维护一个缓冲区,把上一个chunk的尾部保留下来与当前chunk拼接后再搜索。Buffer的固定长度特性意味着拼接会产生新Buffer,这是流式协议解析中需要主动管理的开销。
延伸阅读与参考文献
Node.js 官方文档—buf.includes():权威API参考,包含参数说明、等价关系声明和示例代码。https://nodejs.org/api/buffer.html#bufincludesvalue-byteoffset-encoding
Node.js 中文文档—buf.includes():官方文档的简体中文版本,便于对照参数语义。https://url.nodejs.cn/api-v16/buffer/buf_includes_value_byteoffset_encoding.html
Node.js 官方文档—buf.indexOf():includes()的底层逻辑等价函数,文档中关于数值强制转换、空值处理、类型转换的说明更为详细,是理解includes()边界行为的重要参考。https://nodejs.org/api/buffer.html#bufindexofvalue-byteoffset-encoding
Node.js 官方文档—buf.lastIndexOf():从后向前的搜索变体,与includes()和indexOf()构成完整的Buffer搜索方法组。https://nodejs.org/api/buffer.html#buflastindexofvalue-byteoffset-encoding
MDN—TypedArray.prototype.includes():标准TypedArray的includes()语义,理解Buffer特有行为与标准行为差异的参照。https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/includes
Buffer.includes()的规则不复杂:它等价于indexOf()!==-1,接受字符串、Buffer和字节值,byteOffset支持负数偏移,空值在合法偏移内返回true。容易出问题的地方在于把它当字符串搜索用、忽略编码转换带来的字节差异、以及在跨chunk场景中期待单次调用完成搜索。把includes()、indexOf()、lastIndexOf()的职责分工理清楚,二进制数据的存在性判断就不容易出错。