☰
jstips 第 15 期:用按位非操作符 `~` 把 indexOf 写成更简洁的 contains 判断
2026/10/8 23:41:15 网站建设 项目流程
  • 教程

【免费下载链接】jstips

This is about useful JS tips!

项目地址:https://gitcode.com/gh_mirrors/js/jstips
点击查看免费下载

本文是 jstips 仓库(gh_mirrors/js/jstips)第 15 期技巧的中文版详解,主题为"更简单的使用 indexOf 实现 contains 功能"。JavaScript 原生没有contains方法,本文将从最直观的indexOf !== -1写法出发,拆解按位非操作符~的原理,还原 Express 源码中的真实用法,并延伸到 ES6/ES7 的String.prototype.includes()与Array.prototype.includes(),让你在字符串与数组的包含性判断上写出更简洁、更地道的代码。

传统写法:用 indexOf 判断"是否包含"

JavaScript 并没有为字符串或数组提供开箱即用的contains方法。要检测"一个子字符串是否存在于某个字符串中"或"一个变量是否存在于某个数组中",最常见的做法是借助indexOf的返回值:

var someText = 'javascript rules'; if (someText.indexOf('javascript') !== -1) { // 包含 "javascript" } // or if (someText.indexOf('javascript') >= 0) { // 包含 "javascript" }

原理很简单:indexOf在找到目标时返回首次出现的位置索引(0或更大的非负整数),找不到时返回-1。因此!== -1与>= 0两种写法语义等价,都能区分"包含"与"不包含"。

这两种写法完全正确,也很常见——但它们把"是否包含"这一布尔语义,翻译成了"索引值与 -1 的关系",阅读时需要多一层心智转换。在 ES5 时代,有没有更简洁的等价写法?答案是有的,而且它来自真实的工业级代码。

Express 源码中的真实用法:~操作符

原文档引用了 Express(当时托管于 strongloop 组织下)的三处源码片段,展示了~(按位非)操作符在真实项目中的用法。

片段一:examples/mvc/lib/boot.js中,遍历对象属性时跳过"保留字段":

for (var key in obj) { // "reserved" exports if (~['name', 'prefix', 'engine', 'before'].indexOf(key)) continue; }

这里['name', 'prefix', 'engine', 'before'].indexOf(key)若找到返回索引(0起),找不到返回-1;~会把结果翻转为"找到 → 非零真值,找不到 → 0 假值",于是if (~...) continue;直接把"key 是保留字段"当成条件使用。

片段二:lib/utils.js中的normalizeType,用~type.indexOf('/')判断 MIME 类型字符串是否包含斜杠:

exports.normalizeType = function(type){ return ~type.indexOf('/') ? acceptParams(type) : { value: mime.lookup(type), params: {} }; };

type形如'text/html'时包含/,~indexOf为真,走acceptParams(type)分支;否则走纯 MIME lookup 分支。

片段三:examples/web-service/index.js中,校验 API Key 是否合法:

// key is invalid if (!~apiKeys.indexOf(key)) return next(error(401, 'invalid api key'));

apiKeys.indexOf(key)找不到时返回-1,~(-1)得到0(假值),!0为true,于是进入 401 分支。这一行把"key 不在合法列表中"表达得极为紧凑。

原理拆解:~为什么能当 contains 用

关键在按位非操作符~。MDN 对其的经典描述是:"位操作符操作数字的二进制形式,但返回值依然是标准的 JavaScript 数值。"具体到~,它会先把操作数转换为 32 位有符号整数,然后对所有位取反,其数学本质是:

~x === -(x + 1)

于是对indexOf的两类返回值:

  • 找不到时返回-1:~(-1) === -(-1 + 1) === 0,而0在 JavaScript 中是 falsy(假值);
  • 找到时返回0、1、2……:~0 === -1、~1 === -2、~3 === -4,全部是非零数值,在布尔上下文中均为 truthy(真值)。

也就是说,~indexOf(...)的结果天然地编码了"是否包含"这一布尔语义:包含 → 真值,不包含 → 0(假值)。原文档中的示例代码完整地演示了这一点:

var someText = 'text'; !!~someText.indexOf('tex'); // someText contains "tex" - true !~someText.indexOf('tex'); // someText NOT contains "tex" - false ~someText.indexOf('asd'); // someText doesn't contain "asd" - false ~someText.indexOf('ext'); // someText contains "ext" - true

逐行核对('text'的索引分别是:'tex'→0,'asd'→-1,'ext'→1):

  • ~0 === -1,!!-1→true:包含'tex';
  • ~0 === -1,!-1→false:即"包含"的否定为假(注意:!~表达的是对"包含"取反,因此此处结果false恰说明确实包含);
  • ~(-1) === 0:不包含'asd',条件为假;
  • ~1 === -2:包含'ext',条件为真。

可以看出~写法把indexOf的"数值结果"直接当作条件使用,省略了!== -1的比较,同时!!~与!~还能显式地把结果归一化为布尔值或对结果取反——后者与仓库中第 30 期技巧 将 truthy/falsy 转换为布尔值 中!!的用法一脉相承。

实战建议:~写法的使用边界

~indexOf写法足够简洁,但需要留意它的适用条件与代价:

  • 仅适用于"以 -1 表示未找到"的 API:String.prototype.indexOf与Array.prototype.indexOf均满足"找不到返回 -1",因此可以直接套用;对语义不同的方法(例如返回布尔值的includes)则完全没必要再用~。
  • 位运算有类型转换开销:~会把操作数强制转换为 32 位有符号整数,对超出 32 位范围的索引值理论上存在截断风险(实际业务中的数组/字符串索引几乎不会触及该上限)。在性能敏感的循环里,原生indexOf本身仍是 O(n) 线性扫描,~并不会改变算法复杂度。
  • 可读性权衡:~['a','b'].indexOf(x)是老一辈工程师钟爱的"一行判断",但团队协作时若成员不熟悉位运算,x !== -1反而更直白。建议在代码库中统一风格,或辅以注释说明。

此外,~技巧不止能判断"是否包含",还能用于白名单过滤、参数归一化等场景。仓库第 37 期技巧 数组去重 中同样用到了Array.prototype.indexOf(配合filter实现arr.indexOf(el) === i),可作为索引语义的延伸阅读。

ES6 时代的正解:String.prototype.includes()

如果你不需要兼容老旧浏览器,ES6 引入了专门的原生方法includes(),语义上就是"是否包含":

'something'.includes('thing'); // true

相比~技巧,includes直接返回布尔值,消除了位运算带来的心智负担,也与contains的直觉语义完全一致。String.prototype.includes还支持可选的第二参数position(起始搜索位置),比裸indexOf的"从头搜索"更灵活。

ES7 的数组版:Array.prototype.includes()

ECMAScript 2016(即 ES7)进一步把includes扩展到了数组。原文档将两种写法并列对比:

!!~[1, 2, 3].indexOf(1); // true [1, 2, 3].includes(1); // true

两者结果一致,但includes的意图一目了然。值得注意的是数组版includes与indexOf在"相等性判定"上存在细微差异:indexOf基于严格相等(===)比较,而includes还能识别NaN。例如[NaN].indexOf(NaN)返回-1,而[NaN].includes(NaN)返回true——在需要判断数组中是否存在NaN时,includes是更可靠的选项。

兼容性提醒与选型总结

原文档给出了明确的兼容性警告,这里完整保留并补充说明:

不幸的是,只有 Chrome、Firefox、Safari 9 及其更高版本和 Edge 支持了includes功能,IE11 及其更低版本并不支持。最好在受控的环境中使用此功能。

也就是说:

  • String.prototype.includes与Array.prototype.includes都属于较新的 ES 标准 API,面向 IE11 及以下浏览器时必须借助 polyfill 或降级方案;
  • 在受控环境(如现代浏览器内部工具、Node.js 服务端、Electron 等确定运行时版本的项目)中,应优先使用includes,因为它语义最清晰;
  • 在不可控的浏览器环境中,~indexOf技巧不依赖任何新 API,仅用 ES3 时代的位运算即可实现同样的判断,是兼容性最稳的写法;
  • 若仍使用indexOf,也建议用!== -1这类显式比较,方便他人阅读。

小结

从indexOf !== -1到~indexOf,再到String/Array.prototype.includes,这条演进路径恰好浓缩了 JavaScript 判断"是否包含"的三代写法:

写法适用时代返回值可读性兼容性
indexOf(...) !== -1ES3 起布尔中等极好
~indexOf(...)/!!~indexOf(...)ES3 起真值/布尔较低(需懂位运算)极好
includes(...)ES6(字符串)/ ES7(数组)布尔最高IE11 及以下不支持

本文的完整原始内容对应仓库中的英文原版 _posts/en/javascript/2016-01-15-even-simpler-way-of-using-indexof-as-a-contains-clause.md、中文简体版 _posts/zh_CN/javascript/2016-01-15-even-simpler-way-of-using-indexof-as-a-contains-clause.md、中文繁体版 _posts/zh_TW/javascript/2016-01-15-even-simpler-way-of-using-indexof-as-a-contains-clause.md 及西班牙语版 _posts/es_ES/javascript/2016-01-15-even-simpler-way-of-using-indexof-as-a-contains-clause.md;更多同类技巧可查看仓库 README.md 中的 Tips 列表。实战中请结合团队约定与目标运行时环境,在"简洁"与"可读性"之间做出取舍。

  • 教程

【免费下载链接】jstips

This is about useful JS tips!

项目地址:https://gitcode.com/gh_mirrors/js/jstips
点击查看免费下载
上一篇:Trigger.dev 数据库包完全指南:Prisma Schema、迁移工作流与大表索引最佳实践
下一篇:Ragas 评估结果可视化:两行代码将评测分数接入 Tonic Validate

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询