# 隐式类型转换

[toc]

在js中, 1 + '1' === '11'。为什么不同的数据类型可以进行运算?这是因为js自动的将数据类型进行了转换,这种我们通常称之为隐式类型转换。+运算符既可以用于字符串拼接, 也可以做数字加法,至于具体是什么操作就取决于表达式的参数类型。

# 1、一元操作符

# 1.1 + 加号操作符

当 + 运算符作为一元操作符的时候,会调用 ToNumber 处理该值,相当于 Number('1'),最终结果返回数字 1

查看一下下面的案例

console.log(+[]); // 0
console.log(+[1]); // 1
console.log(+[1, 2, 3]); // NaN
console.log(+{}); // NaN

+[]举个例子, 其实就是Number([])。由于是对象类型,调用valueOf返回本身[],再调用toString 返回 ''Number('')返回0。其他的也都是一个道理。

# 1.2 +日期

+new Date(); // 返回是时间戳

其实就是ToNumber(new Date()),参数是对象的话,ToNumber 会先调用 valueOf, 但是valueOf对于日期实例来说是一个例外, 会调用日期实例的getTime()返回时间戳。可以理解为 Date类的valueOf被重写了

const d = new Date()
d.valueOf() === d.getTime(); // true

# 1.3 - 减号操作符

一元 - 运算符将其操作数转换为 Number 类型,然后将其取反。请注意,对 +0 求反会产生 -0,对 -0 求反会产生 +0。

表达式 - UnaryExpression 的计算步骤如下:

  1. Let expr be the result of evaluating UnaryExpression. 让expr = UnaryExpression
  2. Let oldValue be ToNumber (opens new window)(GetValue (opens new window)(expr)). 对expr调用valueOf。再调用ToNumber。
  3. If oldValue is NaN, return NaN. 如果第二步的返回值为NaN,返回NaN
  4. Return the result of negating oldValue; that is, compute a Number with the same magnitude but opposite sign. 返回oldValue取反的结果;也就是说,计算一个大小相同但符号相反的数字。

看几个例子

console.log(-'1'); // -1
console.log(-'a'); // NaN
console.log(-true); // -1
console.log(-false); // -0
console.log(-[]); // -0
console.log(-{}); // NaN
console.log(-['42']); // -42
console.log(-['1', '2']); // NaN

分析一下 -true, true的valueOf返回本省true, Number(true)返回1, 所以结果是-1。其他的都是同理

# 2、二元操作符+

我们都知道1 + '1'的结果, 那么[] + [],{} + [}],null + 1, [] + {}呢?根据规范可以总结为如下

规范: 11.6.1 (opens new window)当计算 value1 + value2时:

  1. lprim = ToPrimitive(value1)
  2. rprim = ToPrimitive(value2)
  3. 如果 lprim 是字符串或者 rprim 是字符串,那么返回 ToString(lprim) 和 ToString(rprim)的拼接结果
  4. 返回 ToNumber(lprim) 和 ToNumber(rprim)的运算结果
const a = {
  valueOf() {
    return 1;
  }
};

const b = {
  valueOf() {
    return 2;
  }
};
// lprim = 1, rprim = 2, 所以结果是3
console.log(a + b); // 3

# 2.1 null + 数字

null + 1

根据规范的步骤进行分析

  1. lprim = ToPrimitive(null),由于null是基本类型, 所以lprim = null
  2. rprim = ToPrimitive(1),由于1是基本类型, 所以lprim = 1
  3. 两边都不是字符串,直接第4步
  4. 返回返回 ToNumber(null) 和 ToNumber(1)的运算结果.

由于Number(null)等于0, 所以实际就是 0 + 1。因此

null + 1 === 1

# 2.2 数组 + 数组

[] + []

根据规范的步骤进行分析

  1. lprim = ToPrimitive([]),由于[]不是基本类型, 调用valueOf返回本身, 调用toString返回空字符串,所以lprim = ''

  2. rprim = '', 同上

  3. 两边都是字符串,直接第4步,返回 ToString(lprim) 和 ToString(rprim)的拼接结果

所以

[] + [] === ''

# 2.3 数组与对象

[] + {}
  1. lprim = ToPrimitive([]), 由于[]不是基本类型, 调用valueOf返回本身, 调用toString返回空字符串,所以lprim = ''
  2. rprim = ToPrimitive({}), 由于{}不是基本类型, 调用valueOf返回本身, 调用toString返回'[object Object]',所以rprim = '[object Object]'
  3. 由于rprim 是字符串,那么返回 ToString(lprim) 和 ToString(rprim)的拼接结果

所以最后的结果是'' + '[object Object]' = '[object Object]'

# 2.4 对象与对象,数字和布尔

{} + {} === '[object Object][object Object]'
true + 1 => Number(true) + Number(1) => 1 + 1 === 2

# 3、二元操作符 -

二元减操作符的计算步骤如下,规范11.6.2 (opens new window)

  1. 对左边的值调用lval = GetValue(lref)
  2. 对左边的值调用rval = GetValue(rref)
  3. 计算lnum = ToNumber(lval)
  4. 计算rnum = ToNumber(rval)
  5. 返回lnum - rnum的值。

举例

1 - '0' === 1
true - false === 1
1 - [] === 1
1 - {} ==> NaN

# 4、== 相等,宽松相等

== 有时候也叫loose equal。当要比较的两个值, 类型不一致的时候, 就会发生类型的转换。规范11.9.5 (opens new window)

当执行x == y 时:

  1. 如果x与y是同一类型:
    1. x是Undefined,返回true
    2. x是Null,返回true
    3. x是数字:
      1. x是NaN,返回false
      2. y是NaN,返回false
      3. x与y相等,返回true
      4. x是+0,y是-0,返回true
      5. x是-0,y是+0,返回true
      6. 返回false
    4. x是字符串,完全相等返回true,否则返回false
    5. x是布尔值,x和y都是true或者false,返回true,否则返回false
    6. x和y指向同一个对象,返回true,否则返回false
  2. x是null并且y是undefined,返回true
  3. x是undefined并且y是null,返回true
  4. x是数字,y是字符串,判断x == ToNumber(y)
  5. x是字符串,y是数字,判断ToNumber(x) == y
  6. x是布尔值,判断ToNumber(x) == y
  7. y是布尔值,判断x ==ToNumber(y)
  8. x是字符串或者数字,y是对象,判断x == ToPrimitive(y)
  9. x是对象,y是字符串或者数字,判断ToPrimitive(x) == y
  10. 返回false

下面看几种特殊情况

# 4.1 null == undefined

根据规范第2步和第3步, 返回true

null == undefined => true

# 4.2 字符串和数字

根据规范第4步和第5步。得出结论最后都是转换成数字进行比较。

# 4.3布尔值和其他类型

当其中一方是布尔值的时候, 都是把布尔值进行ToNumber处理再进行比较的。比如

true == '1' => true; // 因为Number(true) = 1

为此我们需要尽量减少 xx == true 或者false == xx这种写法。可以采取 ===

# 4.4 对象和非对象

42 == ['42']; // true

直接看规范第8、9步。

这个其实就是 42 == ToPrimitive(['42']), 相当于 42 == '42', 相当于42 == 42,结果为true.

# 4.5 总结一下

TIP

如果不是同一个类型,最后一步都会转换到Number来进行比较。

# 4.6 一些其他情形

  1. false == undefined; // false

    相当于 0 == undefined。不属于任何的情形, 返回false

  2. false == []; // true

    1. false 转换为number就是 0,判断 0 == []
    2. 就是判断0 == ToPrimitive([]), 也就是0 == ''
    3. 到达 x是数字,y是字符串,判断0 == ToNumber(''), 最后是0 == 0
  3. [] == ![]; // true

    会执行![]. 相当于[] == false。 所以返回true

  4. 再看一下其他例子

    console.log(false == "0") // true
    console.log(false == 0) // true
    console.log(false == "")// true
    
    console.log("" == 0)// true// true
    console.log("" == [])// true
    
    console.log([] == 0)// true
    
    console.log("" == [null]) // true [null].toString() 返回空字符串
    console.log(0 == "\n"); // Number('\n') 返回 0
    

# 5、其他隐式情形

除了这两种情形之外,其实还有很多情形会发生隐式类型转换,比如if? :&&等。

# 6、looseEqual

用于判断两个对象是否看起来相等,即存在一样的键值对。

function isObject(a) {
  return a !== null && typeof a === 'object';
}

function looseEqual(a, b, map = new WeakMap()) {
  if (a === b) return true;

  const isObjectA = isObject(a);
  const isObjectB = isObject(b);

  if (isObjectA && isObjectB) {
    // 稍微增强循环引用的处理能力
    if (map.has(a) && map.has(b)) {
      return a === b;
    }
    map.set(a, true);
    map.set(b, true);
    try {
      const isArrayA = Array.isArray(a);
      const isArrayB = Array.isArray(b);
      if (isArrayA && isArrayB) {
        return (
          a.length === b.length &&
          a.every(function (item, index) {
            return looseEqual(item, b[index], map);
          })
        );
      } else if (a instanceof Date && b instanceof Date) {
        return +a === +b;
      } else if (!isArrayA && !isArrayB) {
        const keysA = Object.keys(a);
        const keysB = Object.keys(b);
        return (
          keysA.length === keysB.length &&
          keysA.every(key => {
            return looseEqual(a[key], b[key], map);
          })
        );
      } else {
        return false;
      }
    } catch (e) {
      console.error(e);
      return false;
    }
  } else if (!isObjectA && !isObjectB) {
    return String(a) === String(b);
  }

  return false;
}

上次更新: 1/22/2025, 9:39:13 AM