- 教程
【免费下载链接】jstips
This is about useful JS tips!
本文是 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(...) !== -1 | ES3 起 | 布尔 | 中等 | 极好 |
~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!
相关推荐
jstips 技巧 15:用 `~` 位运算把 `indexOf` 变成优雅的 contains 判断
jstips 技巧 15:用 ~ 位运算把 indexOf 变成优雅的 contains 判断 JavaScript 原生没有提供 contains 方法,日常
教程Arthas `sm` 命令完全指南:Search Method 搜索已加载类的方法信息
Arthas sm 命令完全指南:Search Method 搜索已加载类的方法信息 sm (Search Method 的缩写)是 Arthas 内置的类方法
教程jstips 精讲(第 30 期):用 `!!` 操作符将 truthy/falsy 值转换为布尔值
jstips 精讲(第 30 期):用 !! 操作符将 truthy/falsy 值转换为布尔值 导读 在 JavaScript 开发中, if value 这
教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考