Includes引发的一系列思考

1,018 阅读5分钟

今天看到一段类似下面的代码:

const arr = [msg => console.log('aaa'), msg => console.log('bbb')]

arr.includes(msg => console.log('aaa')) // false

但是,

var funA = msg => console.log('aaa')

var funB = msg => console.log('bbb')

const arr = [funA, funB]

arr.includes(funA) // true

其实有点JS基础的同学应该都知道答案了,上面的例子中看起来都是完全一样的匿名函数,但是指针地址不同,所以是false,下面的例子因为有funcA指向同一个地址,是“同一个对象”,所以结果是true。但是本着打破砂锅问到底的精神,逐步思考。

深入学习includes

MDN中关于Array.prototype.includes()的定义是:includes() 方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false。

语法:

arr.includes(valueToFind[, fromIndex])

valueToFind是需要查找的元素值 ,fromIndex指的是从fromIndex 索引处开始查找 valueToFind。如果为负值,则按升序从 array.length + fromIndex 的索引开始搜 (即使从末尾开始往前跳 fromIndex 的绝对值个索引,然后往后搜寻)。默认为 0。 如果 array.length + fromIndex 仍然是负数,则认为fromIndex是0。

根据MDN中的Polyfill以及tc39中的信息,我们大概可以写出includes的源码:

Object.defineProperty(Array.prototype, 'includes', {

  value: function(valueToFind, fromIndex) {

    if (this === null) {
      throw new TypeError('"this" is null or not defined')
    }

    var obj = Object(this)
    const len = obj.length >>> 0
    if (len === 0) {
      return false
    }
 

    // Let n be ? ToInteger(fromIndex).(If fromIndex is undefined, this step produces the value 0.)
    const num = fromIndex | 0
    const k = Math.max(num >= 0 ? num : len - Math.abs(num), 0)

    function sameValueZero(x, y) {
      return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y))
    }    

    while (k < len) {
      if (sameValueZero(valueToFind, obj[k])) {
        return true
      }
      ++k
    }

    return false
  }
})

上面代码基本参照MDN中的Polyfill,应该说还是很严谨的,其中比较疑惑的两个点:

  • obj.length >>> 0

    • >>>是无符号右移 ,向右被移出的位被丢弃,左侧用0填充。 JavaScript中关于数字的类型是Number,不区分int、long、float、double,Number实际上是一个基于 IEEE 754 标准的双精度64位浮点数 (类似Java的double)。JavaScript进行位运算的时候,会将操作数通通转成 32 位比特序列(补码),操作完成之后,再按照 64 位浮点数存储

    • JavaScript进行位运算会直接丢弃小数部分,所以有个骚操作就是双取反就是取整

    • 对于非数值类型,位运算会先将其转化为整型 (0)然后进行位运算。对非数值变量做取反操作,得到的一定是 -1,因为实际上是在对 0 做取反操作。

    • >>>0实际上没有数位变化,但是js会把符号位换成0。本质上是先转为number,然后将number转换为无符号的32bit数据,也就是Uint32类型。【所有非数值转换成 0,大于等于0的数取整数部分】

  • fromIndex | 0

    • 非数值转换成0,如果是number则丢弃小数部分

includes和indexOf

includes是ES7的新方法,用于检测数组中是否包含某个元素 。Array中类似API:

['aaa', 'bbb'].includes('aaa') // true

['aaa', 'bbb'].findIndex(item => item==='bbb') // 1

['aaa', 'bbb'].find(item => item==='bbb') // bbb

['aaa', 'bbb'].indexOf('bbb') // 1

includes原本叫contains ,但是如果叫contains 会影响很多网站自行hack的contains。其他具体细节可查看includes的提案

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

Array.prototype.includes 底层使用了 SameValueZero() 进行元素比较 ,而indexOf底层使用了严格相等进行元素比较。

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

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

JavaScript 中的相等性判断

ES2015中有四种相等算法

  • 抽象(非严格)相等比较 (==)

  • 严格相等比较 (===): 用于 Array.prototype.indexOf, Array.prototype.lastIndexOf, 和 case-matching

  • 同值零(SameValueZero): 用于 %TypedArray%ArrayBuffer 构造函数、以及MapSet操作, 并将用于 ES2016/ES7 中的String.prototype.includes

  • 同值(SameValue): 用于所有其他地方

JavaScript提供三种不同的值比较操作:

  • 严格相等比较 (也被称作"strict equality", "identity", "triple equals"),使用 ===
  • 抽象相等比较 ("loose equality","double equals") ,使用 ==
  • 以及 Object.is (ECMAScript 2015/ ES6 新特性)

严格相等 ===

全等操作符比较两个值是否相等,两个被比较的值在比较前都不进行隐式转换。如果两个被比较的值具有不同的类型,这两个值是不全等的。否则,如果两个被比较的值类型相同,值也相同,并且都不是 number 类型时,两个值全等。最后,如果两个值都是 number 类型,当两个都不是 NaN,并且数值相同,或是两个值分别为 +0 和 -0 时,两个值被认为是全等的。

NaN === NaN // false
+0 === -0 // true

非严格相等 ==

相等操作符比较两个值是否相等,在比较前将两个被比较的值转换为相同类型。在转换后(等式的一边或两边都可能被转换),最终的比较方式等同于全等操作符 === 的比较方式。 相等操作符满足交换律。

同值相等

确定两个值是否在任何情况下功能上是相同的,方法由Object.is提供

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

零值相等

与同值相等类似,不过会认为 +0 与 -0 相等。

includes的小结

includes原理是SameValueZero

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

开头问题,何解?

对于function类型,使用toString()方法进行比较,这样"看起来一样"的函数再使用includes进行判断就会得到true。【意义不大】

JS中的位操作

js中 something >>> 0是什么意思?

js >>> 0 谈谈 js 中的位运算

JS如何处理超过32位的整数的位运算