先记住这个答案
includes 能检测到 NaN,而 indexOf 不能。includes 使用 SameValueZero 比较,它和 === 基本一样,唯一的区别是把 NaN 视为等于 NaN,所以 [NaN].includes(NaN) 是 true。而 indexOf 使用严格相等,NaN === NaN 是 false,所以 [NaN].indexOf(NaN) 是 -1。除此之外两者对 +0 和 -0 都视为相等,对象都按引用比较。还有一个差异在稀疏数组上:includes 会把空位当成 undefined 参与比较,indexOf 会直接跳过空位。
includes用 SameValueZero,NaN等于自身indexOf用===,永远找不到NaN- 两者都认为
+0和-0相等 - 稀疏数组里
includes把空位当undefined,而indexOf跳过空位
同一个数组,两种结果
把 NaN、-0 和空位放进同一个例子里对比。
NaN、-0 和空位JavaScriptconst arr = [1, NaN, -0]
console.log(arr.includes(NaN), arr.indexOf(NaN)) // SameValueZero 认 NaN,=== 不认
console.log(arr.includes(0), arr.indexOf(0)) // 两者都认为 0 和 -0 相等
const sparse = [1, , 3] // 下标 1 是空位
console.log(sparse.includes(undefined), sparse.indexOf(undefined))查看输出与解释
true -1
true 2
true -1第一行是题目的核心。NaN 表示「不是一个有效数字」,IEEE 754 浮点标准规定它不等于任何值,包括它自己,=== 遵守了这条规则。includes 是 ES2016 才加的,设计时专门处理了这个问题,因为「数组里有没有 NaN」是一个合理的需求。
第三行是空位的差异。includes 从 0 到 length - 1 逐个读值,空位读出来就是 undefined,所以能匹配。indexOf 是更早的方法,它会先检查这个下标上有没有属性,没有就跳过,所以找不到。
三种相等算法放在一起看
JavaScript 里其实有好几套相等规则,不同的 API 用的不一样,记住它们在 NaN 和 ±0 上的差别就够了。
===、Object.is 和 SetJavaScriptconsole.log(NaN === NaN, Object.is(NaN, NaN))
console.log(0 === -0, Object.is(0, -0))
console.log(new Set([NaN, NaN, 0, -0]).size) // Set 用 SameValueZero
console.log([NaN].findIndex(Number.isNaN)) // 找 NaN 的下标查看输出与解释
false true
true false
2
0Object.is 最严格,能区分 +0 和 -0。SameValueZero 介于两者之间:NaN 像 Object.is 那样认自己,±0 像 === 那样当成一个。Set 和 Map 的键也用这个规则,所以上面的 Set 里只剩一个 NaN 和一个 0。
需要 NaN 的下标时,indexOf 做不到,要用 findIndex(Number.isNaN)。注意不要传全局的 isNaN,它会先把参数转成数字,isNaN('abc') 也是 true。
容易答错的地方
- 说
includes用的是Object.is - 如果用的是
Object.is,那么[-0].includes(0)就该是false,但实际是true。所以includes用的是 SameValueZero,名字里的 Zero 就是指它不区分正负零。 - 以为
includes能按内容找对象 [{ id: 1 }].includes({ id: 1 })是false。三种相等算法对对象都只比较引用。按内容找要用some或findIndex,自己写判断条件。- 以为
includes只是indexOf !== -1的简写 - 大多数时候两者可以互换,但
NaN和稀疏数组这两种情况结果不同。代码里只关心「有没有」时,直接用includes,意图更清楚,也不会被这两种边界坑到。
面试官还会怎么问?
字符串的 includes 和数组的 includes 一样吗?
不一样,它们是两个方法。字符串的 includes 查找子串,'abc'.includes('bc') 是 true,而且参数会被转成字符串。数组的 includes 比较的是元素,['abc'].includes('bc') 是 false。
两个方法都有第二个参数,含义一样吗?
一样,都是 fromIndex,表示从哪个下标开始找,负数表示从末尾往前数。比如 [1, 2, 1].includes(1, 1) 从下标 1 开始找,结果还是 true。
老浏览器不支持 includes,怎么兼容 NaN 的情况?
可以用 arr.some((x) => x === target || (x !== x && target !== target))。其中 x !== x 只有在 x 是 NaN 时才成立,这是 Number.isNaN 出现之前判断 NaN 的经典写法。不过如今直接引入 core-js 的 polyfill 更省事。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。