有了 indexOf,为什么 ECMAScript 7 还添加了 Array.prototype.includes

Posted jQuery每日经典

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了有了 indexOf,为什么 ECMAScript 7 还添加了 Array.prototype.includes相关的知识,希望对你有一定的参考价值。

ECMAScript 7 中新增了用于检测数组中是否包含某个元素 Array.prototype.includes() API,想到了 Array其实有很多相关 API 可以检测到是否包含某个元素,比如 Array.prototype.indexOf,于是好奇为什么要实现这样一个 "看起来功能有点重复的 API"。

前言

最近又看了下 ECMAScript 7 规范,看到新的规范中包含 Array.prototype.includes(),方法签名如下:

Array.prototype.includes(value : any): boolean

Array.prototype.includes() 是用于检测数组中是否包含某个元素。

[0, 1].includes(1) // true
['foo', 'bar'].includes('baz') // false

想到了 Array 其实有很多相关 API 可以检测到是否包含某个元素:

[0, 1].findIndex(i => i == 1) // 1
['foo', 'baz'].find(i => i == 'foo') // foo
['foo', 'baz'].indexOf('foo') // 0
  • Array.prototype.findIndex():返回数组中满足提供的测试函数的第一个元素的索引。否则返回 -1

  • Array.prototype.find():返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined

  • Array.prototype.indexOf():返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回 -1

我们可以简单的通过判断实现类似 Array.prototype.includes() 的效果:

export const includes = 
(sources : any[] searchElement: any): boolean => {
   return !!~any.indexOf(searchElement) }

于是好奇为什么要实现这样一个 "看起来功能有点重复的 API"。

查询了 StackOverflow 和 TC39 (Technical Committee 39,javascript 委员会) 的 ECMAScript 提案,找到一些细节。

Array.prototype.includes 前身

早前的 Array.prototype.includes 的提案名为 Array.prototype.contains,但由于有很多网站自行 hack 了 Array.prototype.contains(其实主要是因为 MooTools 导致的),看起来就跟上面的代码类似。

JavaScript 中所有原生提供的方法属性都是 不可枚举的( enumerable ) 的,我们可以通过 Object.getOwnPropertyDescriptor(object: any, prototypeName : String) 来获取这个属性的属性描述符 (Property Descriptor)。

Object.getOwnPropertyDescriptor
(Array.prototype, 'indexOf')
// output {
writable: true,
enumerable: false,
configurable: true,
value: ƒ()
}

给对象赋值,是不会改变原属性的属性描述符,我们可以给 Array.prototype.indexOf 重新赋值,之后获取它的属性描述符,会发现 indexOf 仍是不可枚举的:

Array.prototype.indexOf = () => {
return -1
}
Object.getOwnPropertyDescriptor
(Array.prototype, 'indexOf')
// output { writable: true, enumerable: false,
configurable: true, value: ƒ() }

而这些网站自行 hack 的 contains() 是可以被枚举的,也就是可以通过 for..in 读出来。

发现问题了么?

如果规范实现 contains(),会导致 contains() 无法被 for..in 读出来,而之前自行 hack 的 contains()是可以被读出来的,所以会出现代码没变动,但是在新规范推出后会产生 bug 的情况。

在 Array.prototype.contains 初稿阶段,考虑到新的规范不能让世界上许多现有的网站出问题,所以改名成了 Array.prototype.includes

细节

起源

虽然我们可以使用 indexOf() 来模拟 includes() 的行为,但是 indexOf() 在语义上无法清晰的描述这个场景。

includes() 是明确的判断 "是否包含该项",而 indexOf() 是 "查找数组中第一次出现对应元素的索引是什么,再针对返回的索引进一步处理逻辑",例如下面的代码:

// indexOf

if (~arr.indexOf(1)) {   // do something
}
// includes
if (arr.includes(1)) {   // do something
}

为什么叫做 includes 而不是 has

has 是用于 key 的,而 includes 是检测 value 的:

let foo = new Map()
foo.set('name', 'linkFly')
foo.has('name') // true

SameValueZero

Array.prototype.includes 底层使用了 SameValueZero() 进行元素比较。

目前 ES2015 草案中有四种相等算法:

  • 抽象标准相等比较:实现接口是 == 运算符

  • 严格相等比较:实现接口是 === 运算符,Array.prototype.indexOf 就是使用这种比较

  • SameValueZero():没有直接暴露的接口,内部实现接口是 Map 与 Set

    const foo = new Map()
    foo.set(0, '0') // Map(1) {0 => "0"}
    foo.set('0', 'zero') // Map(2) {0 => "0", "0" => "zero"}
  • foo.get(0) // 0foo.get('0') // zero
  • SameValue():实现接口是 Object.is()

    NaN === NaN // false
    Object.is(NaN, NaN) // true
  • -0 === +0 // true
  • Object.is(-0, +0) // false

和 SameValue() 不同的是,SameValueZero() 不区分 +0 和 -0。而 includes 为了和 JavaScript 其他特性保持一致 所以内部也采用了 SameValueZero 实现。

所以 Array.prototype.includes 也不区分 +0 和 -0 ,当然也可以检测 NaN

[-0].includes(+0) // true
[NaN].includes(NaN) // true
[NaN].indexOf(NaN) // -1

具体的相等比较运算符差异请参阅 MDN - Equality comparisons and sameness。

具体 Array.prototype.includes 实现的细节可以参考 ecma-262/ECMAScript 7 实现规范。


以上是关于有了 indexOf,为什么 ECMAScript 7 还添加了 Array.prototype.includes的主要内容,如果未能解决你的问题,请参考以下文章

ECMAscript5 新增数组内函数

ECMAScript5 之Array

ECMAScript 5 新增的Array方法

ECMAScript6字符串的扩展

JS学习笔记——数组去重

JavaScript数组方法的兼容性写法 汇总:indexOf()forEach()map()filter()some()every()