从[]+{}和[]-[] 看JavaScript中的隐式类型转换

165 阅读7分钟

基本概念

Primitive 类型

即基本类型,除开对象外的六种类型

  • null
  • undefined
  • boolean
  • number
  • string
  • symbol 在js运算符号中,大多都是将对象转为基本类型,再进行计算

ToPrimitive

一个js内部的方法,作用是将对象转换为基本类型,接受一个输入值input 和一个可选的hint,hint支持两个值,'string','number',大致转换规则如下:

  • 如果input 为基本类型,不转换直接返回

  • 如果hint为'number'

    • 1.调用input的valueOf方法,如果返回的是基本类型,则直接返回,若不是则继续往下
    • 2.调用input的toString()方法,如果返回的是基本类型,则直接返回,若不是继续往下
    • 3.以上两步没得到基本类型,则抱typeerror错误
  • 如果hint为'string'

    • 1.调用input的toString()方法,如果返回的是基本类型,则直接返回,若不是继续往下
    • 2.调用input的valueOf方法,如果返回的是基本类型,则直接返回,若不是则继续往下
    • 3.以上两步没得到基本类型,则抱typeerror错误
  • 如果hint为空

    • 1.如果input为Date类型,则按hint='string'处理
    • 2.否则按照hint='number'处理

toNumber

js内部的方法,作用是将对象转换为数值类型,接受一个输入值input,input类型与返回结果如下表

  • Undefined 结果为 NaN
  • Null 结果为0
  • Boolean 如果参数是 true,结果为 1。如果参数是 false,此结果为 +0
  • Number 结果等于输入的参数(不转换)
  • String 这里es5的规范些了一堆,总结下就是
  • Object
    • 1.按照hint为'number'调用ToPrimitive,得到result。
    • 2.返回 ToNumber(result)。

运算符

加法运算符

在js里面加法运算符比较特殊,既能代表数字相加操作,也能代表字符串拼接,具体机制总结一下

  • 先将左右两边操作数调用ToPrimitive
  • 如果两个任何一个为string类型,则执行字符串拼接操作
  • 返回两个数toNumber后相加的值 到这里我们已经可以解决[]+{}这个问题了,步骤拆解如下
  • 对[]调用ToPrimitive
    • 1.[].valueOf()返回自身
    • 2.[].toString()返回''
    • 3.返回''
  • 对{}调用ToPrimitive
    • 1.[].valueOf()返回自身
    • 2.[].toString()返回'[object Object]'
    • 3.返回'[object Object]'
  • 因为两边都是字符串,所以执行字符串拼接操作最后结果为'[object Object]' 还有诸多类似的情况
console.log([]+[]) // ''
console.log(1+{}) // '1[object Object]'
console.log([]+1) // '1'
consoke.log(undefined + 1) // NaN
const a = {}
const a = {
  valueOf () {
    return {}
  },
  toString () {
    return 'obj a'
  },
}
const b = {
  valueOf () {
    return 1;
  },
  toString () {
    return 'obj b';
  }
}
console.log(a + b) //'obj a1'

减、乘、除

这三个运算符只适用于数字,所以适用于将两边操作数同时调用toNumber转换为数字后再运算

console.log([]-[]) // 0
console.log([]-{}) // NaN
console.log([] - '11') // -11

还有几个一元运算符,也是直接应用toNumber转换后在运算,++,--,+,-

console.log(+[]); // 0
console.log(-[]); // -0
let a = [];
let b = {};
a++;
b--;
console.log(a) // 1 

console.log(b) // NaN

const obj = {
  valueOf () {
    return 10;
  },
  toString () {
    return 100;
  }
}

console.log(+obj); // 10

在加减乘除进行运算的时候有一个坑会被经常提到,就是[]+{}{}+[]放回的是不同的结果,刚开始我也很懵逼,拆了下资料后才发现是代码块在搞鬼,原来在js中,{}不仅可以被当成对象字面量,还可以被看作代码块,在前面没有任何操作符的情况下,就被当做了代码块,所以{}+[]其实相当于+[],执行的是toNumber操作,所以结果为0

宽松相等和严格相等

在js类型中还有一个比较坑的点就是宽松相等的时候的转换,关于宽松相等和严格相等,常见的误区,包括我之前也有,就是“==检查值是否相等,===检查值和类型是否相等”,正确的解释应该是“==允许在相等比较中进行强制类型转换,而===不允许”。如果比较的两个值类型相同。则在处理上几乎无差别,而如果两边值类型不同,===不会做类型转换,==某些情况下会做类型转换

字符串和数字之间

在es5中规范是这样定义

  • 如果Type(x)是数字,Type(y)是字符串,则返回x == toNumber(y)的结果
  • 如果Type(x)是字符串,Type(y)是数字,则返回toNumber(x) == y的结果 就是将字符串转为数字后再比较,我们可以用NaN这个特殊的数字来验证下
console.log(NaN == 'NaN') // false

这里如果是数字NaN转为字符串'NaN'最后的结果应该是true

其它值和布尔值之间的比较

在说布尔值之前我们先看一个例子

console.log('42' == true);  // false
console.log('42' == false); // false

有没有一种离谱的感觉,一个值非真也非假,关于布尔值和其它的的比较,在es规范中是这样说的

  • 如果Type(x)是布尔值,则返回toNumber(x) == y的结果
  • 如果Type(y)是布尔值,则返回x == toNumber(y)的结果 '42' == true分析步骤如下
  • '42' == toNumber(true)得到结果'42' == 1
  • toNumber('42') == 1得到结果42 == 1
  • 得到结果false

'42' == false的转换也一样,先将false转为0,再将'42'转为42最后得到42==0为false

鉴于这种隐晦的规则,个人强烈不建议在开发中用到==布尔值这种判断,一不小心可能就掉坑里了

null和undefined之间的比较

在==中,null和undefined相等,同时他们与自身相等, 除此之外,与任何值不相等

const a = null;
const b = undefined;
console.log(a == b) // true;
console.log(a == null) // true;
console.log(b == undefined) // true;
console.log(b == false) // false;
console.log(a == false) // false;

这一点在开发中很实用,比如我们要判断一个变量既不是null也不是undefined的时候,就可以很使用

if (a == null) {
    ....
}
//相当于
if (a === null || a === undefined) {
    ....
}
对象和非对象之间的相等比较

关于对象和基本类型之间的比较,es规范中定义如下

  • 如果Type(x)是基本类型,Type(y)是对象,则返回x == toPrimitive(y)的结果
  • 如果Type(x)是对象,Type(y)是基本类型,则返回toPrimitive(x) == x的结果

举例说明一下

console.log(42 == [42]) // true
console.log([] == false) // true

42 == [42]

  • 首先42 == toPrimitive([42])得到42 == '42';
  • 然后42 == toNumber('42') 最后得到结果 true;

[] == false

  • 首先toPrimitive([]) == false得到'' == false;
  • 然后'' == toNumber(false) 得到 '' == 0;
  • toNumer('') == 0 得到结果0==0返回true

抽象关系比较

如果你到这里已经感觉已经有点晕了,那也无妨,下面再来一点,加深眩晕效果,关于抽象关系比较,es规范中给出的标准是这样的

  • 比较双方调用toPrimitive,
  • 如果结果类型都是字符串或数字,返回比较结果,
  • 如果一个字符串一个数字,调用toNumber转为数字后再返回结果
const a = [42];
const b = ['43'];
console.log(a < b) // true
console.log(a > b) // false

有一个比较坑的点是两个对象相比较的时候会出现有趣的结果

const a = {};
const b = {};
console.log(a > b); // false
console.log(a >= b); // true
console.log(a == b); // false

是不有有一种被数学欺骗了的感觉,其实仔细一分析就能知道为啥,对象调用toPrimitive转换后得到的结果都是"[object Object]"所以>=的时候是相等的,而在==比较的时候,因为两边都是对象,且并非指向同一个,所以反悔了false;

总结

JavaScript中的类型转换是被很多人诟病的地方,隐式转换往往发生在一些操作的副作用中,难为人所发现,如果掌握不好,会给我们带来一些隐藏的风险!但是只要我们掌握了其中的规则,还是能找到一些精华的地方

参考资料

《你不知道的JavaScript 中卷》