先记住这个答案
Array.prototype.includes 使用 SameValueZero 比较算法:除 NaN 视为等于自身外,其余行为与 === 一致(+0 和 -0 视为相等)。因此 [NaN].includes(NaN) 返回 true。indexOf 使用严格相等比较,NaN === NaN 为 false,所以 [NaN].indexOf(NaN) 返回 -1。两者对 +0、-0 的处理相同,对引用类型的比较语义也一致;主要差异在 NaN 上,另外在稀疏数组上 includes 会把空位当作 undefined 参与比较,而 indexOf 会跳过空位。
includes用 SameValueZero,能匹配NaNindexOf用===,NaN永远返回 -1- 对
+0、-0和引用,两者行为一致
SameValueZero 与严格相等在 NaN 上的分歧
indexOf 内部按索引逐个用严格相等 === 比对目标值。ECMAScript 规定 NaN 不等于自身,这是 IEEE 754 浮点数规范中 NaN 的定义使然,任何 === 比较都无法命中 NaN。所以无论数组里有多少个 NaN,indexOf(NaN) 的结果恒为 -1,这不是实现缺陷而是规范行为。
includes 是 ES2016 引入的,内部使用 SameValueZero 算法。它在 === 基础上只改了一条规则:NaN 视为与自身相等。+0 与 -0 依然视为相等,这一点和 === 一致。Set 和 Map 的键比较也用 SameValueZero,所以 new Set([NaN]).has(NaN) 同样为 true。
const arr = [1, NaN, -0, 'x'];
console.log(arr.includes(NaN));
console.log(arr.indexOf(NaN));
console.log(arr.includes(0));
console.log(arr.indexOf(0));
console.log(NaN === NaN);
console.log(Object.is(NaN, NaN));查看输出与解释
true
-1
true
2
false
true可直接在任意现代 JS 环境运行。indexOf(0) 命中 -0 返回索引 2,说明零值符号不影响两个方法;Object.is 是 SameValue 算法,与 SameValueZero 的区别只在零值符号。
清洗数值列表时判断是否存在非法计算结果
某数据处理模块接收用户表单数值,解析后得到 [12, NaN, 7],其中 NaN 来自 Number('abc') 的转换失败。提交前需要判断是否存在解析失败项并提示用户。输入约束:数组元素均为 number 类型,长度在数千以内。决策:用 values.includes(NaN) 做存在性检查,命中则拦截提交并定位错误字段。
若写成 values.indexOf(NaN) !== -1,条件永远为假,非法数据会绕过校验直接入库,属于隐蔽的逻辑错误。选择 includes 后判断语义正确,时间复杂度 O(n),与 indexOf 相同,没有额外代价。这也是代码评审中值得固定下来的约定:数值数组的存在性检查一律用 includes。
该差异失效或不够用的条件
在一般数组(无稀疏空位)上,算法差异只在目标是 NaN 时才体现。若搜索的是对象、数组等引用类型,两个方法都只比引用同一性,includes({a:1}) 找不到数组中结构相同但不同引用的对象。若目标是 0,两者都把 -0 当命中;需要区分 +0 与 -0 时只能用 Object.is 配合 findIndex,includes 无能为力。
另外 includes 只返回布尔值,拿不到位置;需要 NaN 的下标时,indexOf 帮不上忙,应改用 findIndex(Number.isNaN)。稀疏数组上 includes 把空位当作 undefined 参与比较,[,].includes(undefined) 为 true,这一点容易被误判为数组里真的有显式的 undefined 值。
const arr = [1, NaN, -0];
console.log(arr.findIndex(Number.isNaN));
console.log(arr.findIndex(function (v) { return Object.is(v, -0); }));
console.log([,].includes(undefined));查看输出与解释
1
2
true可直接运行。findIndex 接收断言函数,配合 Number.isNaN 或 Object.is 能表达 includes 无法表达的精细比较;空位按 undefined 处理是 includes 的规范行为。
容易答错的地方
- 认为 indexOf(NaN) 能找到第一个 NaN
indexOf内部用===比较,NaN === NaN为false,结果恒为-1。需要位置时应写findIndex(Number.isNaN),需要存在性时用includes(NaN)。- 以为 includes 与 indexOf 只差返回值
- 除了返回值(布尔 vs 下标),比较算法也不同:
includes用 SameValueZero 能匹配NaN;indexOf用===不能。对+0/-0和引用类型两者才完全一致。
面试官还会怎么问?
`Set` 和 `Map` 判断 `NaN` 键时用哪种语义?
两者都用 SameValueZero,与 includes 一致:new Set([NaN]).has(NaN) 为 true,+0 与 -0 视为同一键。这与 Object.is 的 SameValue 不同,后者区分零值符号。
如何不用 includes 判断一个数组是否含 NaN?
可用 arr.some(Number.isNaN)。Number.isNaN 不做类型转换,只对真正的 NaN 返回 true,比全局 isNaN 安全;语义与 arr.includes(NaN) 等价,但 includes 写法更直观。
`includes` 的 fromIndex 参数有什么注意点?
负值表示从末尾倒数的位置,但搜索方向仍从头到尾;fromIndex >= length 时直接返回 false 不搜索。若负值绝对值超过长度,按 0 处理即搜全数组,例如 [1,2,3].includes(1, -100) 为 true。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。