侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 172 篇文章
  • 累计收到 7 条评论

NaN 能被 includes 找到,indexOf 却说没有

2026-10-11 / 0 评论 / 3 阅读

NaN 能被 includes 找到,indexOf 却说没有

一批价格数据从接口回来,中间夹了几个 NaN。用 Set 去重,长度只掉了一个;换成 indexOf 找 NaN 的下标,返回 -1,好像它压根不在数组里。同一个数组,两段代码给出相反的答案。

差别不在数组,在判据。JavaScript 里做相等判断有三条路:===、Object.is,以及 Set 与 Map 内部用的那一条,规范里叫 SameValueZero。三条路大多数时候答案相同,只在几个特殊值上分叉,NaN 和正负零正好都在里面。

三条判据在同一点上分叉

=== 最常用,它说 NaN 和谁都不同,连自己都不同。Object.is 认为 NaN 与自己相同,另外把 +0 和 -0 判成不同的值。SameValueZero 落在中间:认 NaN 相同,也不区分正负零。数组的 indexOf 走 ===,includes 走 SameValueZero,开头那个反差就是这么来的。

[NaN].indexOf(NaN)     // -1,按 === 比
[NaN].includes(NaN)    // true,按 SameValueZero 比

NaN 在容器里的两种命运

把这几条判据并排跑一次,输出如下。

== 1. NaN:=== 为假,includes 为真 ==
NaN === NaN                : false
[NaN].indexOf(NaN)         : -1
[NaN].includes(NaN)        : true
[NaN, 1].lastIndexOf(NaN)  : -1
new Set([NaN, NaN]).size   : 1
new Set([NaN]).has(NaN)    : true
Object.is(NaN, NaN)        : true

indexOf 和 lastIndexOf 都返回 -1,includes 返回 true。Set 那边更直接:两个 NaN 进去只剩一个,has(NaN) 为真。要回答「数组里有没有这个值」,先想清楚要的是哪一条判据,这两个方法的答案会不一样。

容器用的都是 SameValueZero,跟 includes 一致。new Set([NaN, NaN]).size 是 1,new Map([[NaN, 'x']]).get(NaN) 也能取到值。NaN 在 === 的世界里找不到自己,在容器的世界里反而被当成同一个。

正零、负零,还有绕一圈的 JSON

负数除出来的零和普通零,三条判据给出两个答案。

== 2. 正零与负零:=== 不区分,Object.is 区分 ==
0 === -0                   : true
Object.is(0, -0)           : false
[0].indexOf(-0)            : 0
[0].includes(-0)           : true
new Set([0, -0]).size      : 1
new Set([0, -0]).has(-0)   : true
1/0 与 1/-0                : Infinity / -Infinity

== 3. 类型不同一律不等,Set 与 Map 也不合并 ==
[1].includes('1')          : false
[1].indexOf('1')           : -1
new Set([1, '1']).size     : 2
new Map([[1,'num'],['1','str']]).size : 2
Map.get(1) 与 Map.get('1') : num / str

0 === -0 为真,Object.is 为假,includes 与 indexOf 都按相同处理。数字和字符串之间更彻底,三条判据全都不做转换,1 和 '1' 在 Set 里占两个位置,在 Map 里是两个键。

这一条平时看着不会撞上,数据过一次 JSON 就撞上了。JSON.stringify([0, -0, NaN]) 实测得到 [0,0,null]:负零写成 0,NaN 直接变成 null。序列化把两处边界同时抹平,读回来之后再去比,跟原值已经不是一回事。

对象只比引用,去重要另想办法

== 5. 四种判据逐项对照 ==
判据                         NaN,NaN   0,-0    1,'1'   同一引用   结构相同的两个对象
===(indexOf 用的)        : false   true   false  true      false
Object.is                  : true    false  false  true      false
SameValueZero(includes)  : true    true   false  true      false

结构完全一样的两个对象,三条判据都给 false,最后那两列也说明了原因:判据在比之前不看内容。这在去重里最容易被忽略。

== 6. 放进实际写法里看 ==
new Set([{id:1},{id:1}]).size : 2  (对象引用不同,去不掉)
new Set([NaN, NaN]).size      : 1  (NaN 反而去掉了)
[NaN, 0, -0, 1] 里 indexOf 找不到的个数 : 1
list.includes(-0)                      : true
JSON.stringify([0, -0, NaN])           : [0,0,null]

new Set([{id: 1}, {id: 1}]) 的长度是 2,两个对象引用不同,去不掉;反倒是 NaN 被干净地合并成一个。要按内容去重,得先把用来判断的那几个字段拼成一个稳定的键,再拿这个键进 Set。键是字符串之后,比较回到值比较,和引用无关。

两种情况下的选择

判断「值是不是相同」,用 includes 或者 Set;判断「是不是同一个引用」,用 indexOf。反过来用会得到看起来很对、边界上错得很难查的结果。

这里给不出「永远用哪一条」的结论。同一份数据在别处被当成引用还是被当成值,决定权在调用方手里,我倾向于在函数入口就把类型收干净,后面就不用再想这件事。唯一想提醒的是别把 indexOf(v) === -1 当成「不存在」的通用判据,碰上 NaN 它会说谎。

    🤞 分享