# 隐式类型转换
[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
的计算步骤如下:
- Let expr be the result of evaluating UnaryExpression. 让expr = UnaryExpression
- Let oldValue be ToNumber (opens new window)(GetValue (opens new window)(expr)). 对expr调用valueOf。再调用ToNumber。
- If oldValue is NaN, return NaN. 如果第二步的返回值为NaN,返回NaN
- 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时:
- lprim = ToPrimitive(value1)
- rprim = ToPrimitive(value2)
- 如果 lprim 是字符串或者 rprim 是字符串,那么返回 ToString(lprim) 和 ToString(rprim)的拼接结果
- 返回 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
根据规范的步骤进行分析
- lprim = ToPrimitive(null),由于null是基本类型, 所以lprim = null
- rprim = ToPrimitive(1),由于1是基本类型, 所以lprim = 1
- 两边都不是字符串,直接第4步
- 返回返回 ToNumber(null) 和 ToNumber(1)的运算结果.
由于Number(null)
等于0, 所以实际就是 0 + 1
。因此
null + 1 === 1
# 2.2 数组 + 数组
[] + []
根据规范的步骤进行分析
lprim = ToPrimitive([]),由于[]不是基本类型, 调用
valueOf
返回本身, 调用toString
返回空字符串,所以lprim = ''rprim = '', 同上
两边都是字符串,直接第4步,返回 ToString(lprim) 和 ToString(rprim)的拼接结果
所以
[] + [] === ''
# 2.3 数组与对象
[] + {}
- lprim = ToPrimitive([]), 由于[]不是基本类型, 调用
valueOf
返回本身, 调用toString
返回空字符串,所以lprim = '' - rprim = ToPrimitive({}), 由于{}不是基本类型, 调用
valueOf
返回本身, 调用toString
返回'[object Object]'
,所以rprim = '[object Object]' - 由于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)
- 对左边的值调用lval = GetValue(lref)
- 对左边的值调用rval = GetValue(rref)
- 计算lnum = ToNumber(lval)
- 计算rnum = ToNumber(rval)
- 返回lnum - rnum的值。
举例
1 - '0' === 1
true - false === 1
1 - [] === 1
1 - {} ==> NaN
# 4、== 相等,宽松相等
== 有时候也叫loose equal。当要比较的两个值, 类型不一致的时候, 就会发生类型的转换。规范11.9.5 (opens new window)
当执行x == y 时:
- 如果x与y是同一类型:
- x是Undefined,返回true
- x是Null,返回true
- x是数字:
- x是NaN,返回false
- y是NaN,返回false
- x与y相等,返回true
- x是+0,y是-0,返回true
- x是-0,y是+0,返回true
- 返回false
- x是字符串,完全相等返回true,否则返回false
- x是布尔值,x和y都是true或者false,返回true,否则返回false
- x和y指向同一个对象,返回true,否则返回false
- x是null并且y是undefined,返回true
- x是undefined并且y是null,返回true
- x是数字,y是字符串,判断x == ToNumber(y)
- x是字符串,y是数字,判断ToNumber(x) == y
- x是布尔值,判断ToNumber(x) == y
- y是布尔值,判断x ==ToNumber(y)
- x是字符串或者数字,y是对象,判断x == ToPrimitive(y)
- x是对象,y是字符串或者数字,判断ToPrimitive(x) == y
- 返回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 一些其他情形
false == undefined; // false
相当于 0 == undefined。不属于任何的情形, 返回false
false == []; // true
- false 转换为number就是 0,判断 0 == []
- 就是判断0 == ToPrimitive([]), 也就是0 == ''
- 到达 x是数字,y是字符串,判断0 == ToNumber(''), 最后是0 == 0
[] == ![]; // true
会执行![]. 相当于
[] == false
。 所以返回true再看一下其他例子
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;
}