为什么[] == ![]结果为true?

3,320 阅读4分钟

一、前言

先来一个题目来验证自己对于隐式转换的熟悉情况:

console.log({} - {})
console.log([] - [])
console.log([] + [1, 2])
console.log([] == ![])
console.log({} == {})

返回的结果: NaN、0、1,2、true、false

如果这几道题是否是毫无压力呢? 硬背是没有出路,必须要掌握它内在的运行逻辑才能背得更牢.

明确一点,隐式转化其实有一定难度的,涉及到的知识点并不少.有 原型链的知识点、有call的知识点、自然也涉及到了 This指向问题、构造函数等问题. 简单一点的 运算符优先级也会在学习隐式转化的时候涉及. 隐式转化是一个综合性的问题.

所以说,真正的明白了隐式转化的程序员已经有一定的知识积累了. 你要自信😊

二、包装类

包装类是我们理解隐式转化的基础工具,是理解隐式转化的根基.

Boolean()

Boolean只有两个值: truefalse.

Boolean(xxx)false的类型叫做falsey, 或者是虚值.

包括这些0、null、undefined、false、''、NaN

有时候把+0和-0算成两个

其他返回的都是true. 叫做truth.

⚠️需要注意的是, 不要不把Boolean()不当成函数, 不把new Boolean()不当成构造函数实例化的过程:

  if (Boolean(false)) {
    // no execute
  }

  if (new Boolean(flase)) {
    // execute
  }

  console.log(typeof Boolean(false)) // boolean
  console.log(typeof new Boolean(false)) // Boolean

❗️不能说包装了一层包装类的说法,就能够脱离构造函数的相关处理逻辑

Number()

Number()也是有两种类型的结果. 分为两种的处理逻辑. 对于 基本类型的处理, 和对于 引用类型的处理.

基本类型

console.log(Number(null)) // 0
console.log(Number(undefined)) // MaM
console.log(Number('1')) // 1
console.log(Number('')) // 0
console.log(Number(NaN)) // NaN
console.log(Number(false)) // 0
console.log(Number(true)) // 1

字符串转数字,注意Number('1ad')NaN, 一旦字符串中存在非数字的就转化NaN. parseInt()是对 Number()很好的补充.

nullundefined的差别是让人诧异的. 但是如果从作者最初设计的规则来看的话,也是合理的: null表示"没有对象",即该处不应该有值。 undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。

理解null和undefined的区别是需要极大的计算机素养的. 但是有大佬说得对:

虽然这两个东西的区别确实会令初学者困扰,但掌握并理解这两个值的语义实际上和理解 prototype/scope 一样是非常重要的。

引用类型

假设我们有下面这么一个对象:

  const obj = {
    toString() {
      return 2
    },
    valueOf() {
      return 1
    }
  }

当我们使用Number()去包围它的时候, 返回的结果是1.

此时我修改valueOf() { return {} }的时候.再使用Number()去包围它的话,结果却是2了.

如果我们让valueOf() 返回的是基本类型的话, 那么是和直接使用Number()包裹住valueOf()的值一样,即上面基本类型的结果.

所以我们很容易得出如下这样的一个逻辑过程:

  1. 如果valueOf返回原始值,就Number包装之后返回
  2. 如果valueOf返回的对象,就去toString()方法中找
  3. 如果toString() 返回原始值,就Number包装之后返回
  4. 如果toString()返回的是对象,且是自己重写的.那么就直接报错
  5. 如果不是重写的,那么就调用Obejct.prototye.toString方法

这个也是我们让a ==1 && a == 2 && a == 2成立的问题的解决方案之一.

String()

Object.prototype.toString

对于String()的使用依旧使用Number()使用的例子

  const obj = {
   toString() {
    return 2	
  },
   valueOf() {
    return 1	
   }
  }

当我触发String(obj)的时候,就和Number()完全相反.

console.log(String(obj)) // 2

直接访问的是toString()方法.

  const obj = {
      toString() {
        return {}
      },
      valueOf() {
        return 1	
      }
  }
  console.log(String(obj)) // 1

但是如果toString() 返回的是引用类型的话, 就往valueOf()方法上面找. 可以说和Number()的完全相反,但是也符合情理 .

通过重写toString()valueOf()的方法来了解内部的运行规则是一种很好的方式.

如果不重写的话,Object.prototype.toString.call(对象), 返回值参看Number()部分的内容.

console.log(String({})) // [object Object]
3 < 2 < 1 和 2 < 1 < 1

两个输出都是 true . 第一个还可以说看成是数学的方法.第二个就涉及到了隐式转换了.

  1. 比较 2 < 1的结果, 结果为 false. 此时等式为 false < 1
  2. 比较运算符, 将 falseNumber 包装类包起来. 得出的结果是 0.
  3. 0 < 1 自然是 true.

Array.prototype.toString

这个记忆上没啥好说的, 直接把外面的[]给拆了就行.

console.log(String([1])) // '1'
console.log(String([1, 2])) // '1, 2'
console.log(Array.prototype.toString.call([1])) // '1'
console.log(Array.prototype.toString.call([1, 2])) // '1, 2'

三、隐式转化触发规则

boolean的隐式转化触发

if、switch、while、for(;;)、&&、||、!、!!、? : 三元

number的隐式转化触发

只要有小学的知识都知道运算符,它是用于数字之间的计算的.在JavaScript中也是基本是一样的. + - * % == ~~ & | ~ ^ << <<< 等, 位运算符 、算术运算符

string 的隐式转化触发

+ 且两边大于等于1个string类型. 除了有 symbol类型之外.

console.log(1 + '2' + '2') // '122'
console.log(1 + + '2' + '2') // '32'
console.log('A' - 'B' + '2') // 'NaN2'
console.log('A' - 'B' + 2) // NaN

分析这串代码:

  1. 第一个很简单, 符合string隐式转化的条件, +号两边都有至少一个字符串, 所以全部String() 包裹之后再处理
  2. +'2' 是一元运算符, 先Number(2). 所以这里变成了 1 + 2 + '2', 那么就是前面两个先进行运算.
  3. 'A' - 'B' 很显然不符合条件.所以触发的是Number()的隐式转化
  4. 同上

四、需要注意的点:

console.log((123).toString()) // 123
console.log(undefined.toString()) // 报错
console.log(null.toString()) // 报错

undefined和null并没有包装类,它们是基础类型,所以没有toString()方法

五、一道让我掉过坑的面试题:

console.log([] == ![]) // true

!的运算符比==的高.所以这个代码可以分成三部分. []==,以及![].

  1. 看到 等号 这个比较运算法就应该明白 等号 两边都要转化成Number类型
  2. 从左到右的话,Number([]), [] 是引用类型,无法直接拿到原始值
  3. valueOf拿不到值,就走Array.prototype.toString.call([]).从上面可以知道, 它返回的是去掉[],即字符串''.
  4. 此时左边为Number(''). 所以左边返回的自然是0.

此时转变成0 == ![]. 接下来转化右边的:

  1. Boolean()一节当中,就可以知道,除了falsey之外,其他都是ture.而此时在!的加持下,[]会进行Boolean()
  2. 此时右边为true. !true就为false
  3. Number(false)的结果为0

由此得出 0 == 0, 返回结果为true.

console.log({} == {}) // false
console.log({} != {}) // true
  1. 两边都Number()包裹住.
  2. toString()之后都是[obejct Object]
  3. Number('[obejct Object]')NaN
  4. 所以最后转化为console.log(NaN == NaN)的比较

NaN和任何一个值比较都不想等,包括它自己

六、另外一道让我不解的题目

66.toString() // 报错: Invalid or unexpected token
66..toString() // '66'
66.6.toString() // '66.6'
66...toString() // 报错: Unexpected token '.'

个人理解:

状态机当中,碰到数字加上 . 会默认后面跟着的也是数字.默认是0. 即 6.转化为6.0.所以当.之后加入状态机的是toString()之一,JS引擎自然不认识这样玩意.

但是连续两个.之后再跟着toStirng()的话,状态机就认为浮点数已经完成了.所以此时的第二个.就进行了隐式转换.

由此推断三个.过的过程是. 浮点数 加 . 后面跟着又跟着一个.的话, 没有这样的方法,自然就报错了.

项目中还不上TS等啥呢?