隐式转换可谓是JS中的一大难点之一,也是我们平时写代码遇见最多的东西。如果搞不清楚,我们就不能完全掌控我们的代码,因为不知道它会有什么结果。今天我们就来好好唠唠这玩意。
抽象操作
ES5规范的第9节中定义了一些抽象操作(abstract operation),这些操作仅供内部使用。通俗的解释就是我们不能去调用他们,你可以理解为这只是一个规则,用于隐式转换的规则,这些规则都有他们自己的名字,方便理解记忆。
- ToString
- ToNumber
- ToBoolean
- ToPrimitive
这里我们只是简单的介绍,想要知道更具体的转换规则可以参考ES5规范第9节。
ToString
| 参数 | 结果 |
|---|---|
| undefined | “undefined” |
| null | "null" |
| boolean | "true" or "false" |
| number | 123 -> "123",和Number()类似 |
| string | 不需要转换 |
| object | 根据ToPrimitive得到基本类型再进行上述操作 |
ToNumber
| 参数 | 结果 |
|---|---|
| undefined | NaN |
| null | 0 |
| boolean | true -> 1 & false -> 0 |
| number | 不需要转换 |
| string | "123" -> 123 |
| object | 根据ToPrimitive得到基本类型再进行上述操作 |
ToBoolean
JavaScript 中的值可以分成两类:
- 可以被强制类型转换为 false 的值
- 被强制转换成 true 的值
而被强制转换成 false 的值是下面这些:
- undefined
- null
- false
- +0,-0,NaN
- ""
也就是说,除此之外,在通常情况下,其他值均会被转换成 true 。比如对象均会被转换成 true 。
既然我们说了通常情况,那肯定也有不通常情况。浏览器在某些特定情况下,在常规 JavaScript 语法基础上自己创建了一些外来值,这些被称为假值对象。
假值对象和普通对象没有太多差别,但是将他们强制类型转换为布尔值是结果为 false 。比如 document.all 。我们这里只是了解就行了,不作详细讨论。
大部分情况下除了上面所说的会被转换为 false 的值之外都会被转换为 true 。
ToPrimitive
通常我们对对象进行比较操作时,会发生隐式类型转换。ToPrimitive 操作会返回一个基本类型。对于大部分对象,它会先调用 valueOf 方法,如果返回的不是一个基本类型,再调用 toString 方法,如果返回的仍然不是基本对象,会抛出一个错误。
但是对于Date对象,它首先会调用 toString 方法,再调用 valueOf 方法,这里我们不细说了,大家只做了解就行了。
let x = new Date();
let y = {};
let myToString = () => {
return 1;
}
let myValueOf = () => {
return 2;
}
x.toString = y.toString = myToString;
x.valueOf = y.valueOf = myValueOf;
console.log(x == 1);//true
console.log(y == 1);//false
console.log(x == 2);//false
console.log(y == 2);//true
这里给大家一个小例子,感兴趣的可以参考一下,看不懂也没关系,我们继续往下看。
+ 运算符
首先,+ 运算符即能用于数字加法,也能用于字符串拼接,还能表示正数。我们首先讨论 + 用于加法的情况,如何判断是数字加法还是字符串加法。
简单来说,只要 + 的其中一个操作数为字符串,那么就进行字符串的拼接,否则执行数字加法。
如果两边都为数字或者字符串,那么结果是显而易见的。但是如果两边的操作室类型不同,比如一个数字一个字符串。此时就会发生隐式转换,把数字转换成字符串,再进行加法,所以我们得到的结果是字符串。
那么如果有一个操作室是对象,怎么办?此时会根据 ToPrimitive 得到一个基本类型,再进行上述操作。
那么还有一个问题,如果不是字符串也不是对象,是其他的基本类型又怎么办?比如 null 和 undefined。此时会遵守 ToNumber 将基本类型转换为数字再进行上述操作。
而在 + 作为正号表示的时候,会把非 Number 类型的操作数根据 ToNumber 规则转换为 Number 类型。如果是对象就根据 ToPrimitive 规则得到基本类型再根据 ToNumber 得到 Number 类型。
下面我们看一些例子,可以通过这些例子来加深一下自己的理解。
console.log(1 + "1");//11
console.log([1, 2, 3] + 4);//1,2,34
console.log([] + 1);//1
console.log({} + 1);//[object Object]1
console.log(null + 1);//1
console.log(undefined + 1);//NaN
console.log(undefined + true);//NaN
console.log(null + false);//0
console.log(+"hello");//NaN
let obj = {};
obj.valueOf = () => {
return "123";
}
console.log(+obj);//123
1+“1” 会将数字 1 转换为 "1",再进行字符串拼接。
[1, 2, 3] + 4 的 [1, 2, 3]的 toString 会返回 "1, 2, 3",所以拼接后会变成1,2,34。
[]+1 的 [] toString 会返回 "" ,所以数字 1 会变成 "1",最后进行字符串拼接得到 "1"。
{}+1 的 {} 根据规则会返回 [object Object],与1进行拼接得到 [object Object]1。
null+1 的 null 会根据 ToNumber 变成 0 ,和 1 相加以后得到数字 1。
undefined+1 的 undefined 会变成 NaN,和 1 相加以后会得到 NaN。
undefined+true 的 undefined 会变成 NaN,true 会变成 1 ,相加得到 NaN。
null+false 的 null 会变成 0 ,false 也会变成 0,所以最后结果为 0 。
+obj 的 obj 会调用 valueOf 得到 "123",随后再根据 ToNumber 变成数字 123。
- 运算符
-运算符只能用于数字,所以 - 运算符涉及到的操作数都会被转换成 Number 类型。基本类型直接根据 ToNumber 变成 Number 类型,对象根据 ToPrimitive 得到基本类型后再进行上述操作。
console.log("123" - 1);//122
console.log(-"123");//-123
console.log(-"hello");//NaN
let obj = {};
obj.valueOf = () => {
return "123";
}
console.log(-obj);//-123
console.log(-true);//-1
console.log(-null);//-0
console.log(-undefined);//Nan
== 运算符
== 可能是隐式转换最多的一个场景了,下面我们详细讨论一下这种场景
-
字符串和数字比较 ES5规范中这样定义
- 如果Type(x)是数字,Type(y)是字符串,则返回 x == ToNumber(y)的结果
- 如果Type(x)是字符串,Type(y)是数字,则返回 ToNumber(x) == y的结果
即比较双方存在数字时,将字符串转换为数字进行比较。
-
其他类型和布尔值进行比较 规范中这样定义
- 如果 Type(x)是布尔类型,则返回 ToNumber(x) == y的结果
- 如果 Type(y)是布尔类型,则返回 x == ToNumber(y)的结果
即如果比较双方出现布尔值,首先把布尔值变成数字。
-
null和undefined比较 ES5规范中这样定义
- 如果 x 为 null,y 为 undefined,则结果为 true
- 如果 x 为 undefined,y 为 null,则结果为 true
即在 == 操作中,null和undefined相等,除此之外,他们不和任何值相等。
-
对象和非对象比较 ES5规范中这样定义
- 如果 Type(x)是字符串或数字,Type(y)是对象,则返回 x == ToPrimitive(y)的结果
- 如果 Type(x)是对象,Type(y)是字符串或数字,则返回 ToPrimitive(x) == y的结果
-
对象和对象比较 对象和对象比较会比较他们的存储地址是否相同,相同则返回 true,不同则返回false。
-
NaN不等于NaN
-
+0等于-0
你可能会好奇,我们并没有列举完所有情况,比如对象和布尔值比较的情况。但是我们把规则进行组合,布尔值首先会变成数字,在用数字和对象进行比较,就能得到我们的结果了。
下面,我们用一些例子来加深我们的理解
console.log(false == []);
console.log("" == 0);
console.log(0 == []);
console.log([] == ![]);
他们的结果均为 true,我们一条条来分析一下。
- false == [] 首先 false 会被转换成 0,则变成0 == []。随后 [] 作为一个对象,会根据 ToPrimitive 规则得到基本类型,[].valueOf会返回""字符串。即变成了0 == ""。当字符串与数字比较时,会把字符串转换为数字,即“”根据 ToNumber 规则转换为 0,所以最后就是 0 == 0。结果为 true 。
- "" == 0 这条很好理解,我们上一步分析过了0 == ”“的结果为 true 。
- 0 == [] 同样,我们在第一条的分析中也出现过了0 == [],所以他也为 true。
- [] == ![] 这条可能很多人都无法理解,为什么它和它的非还能相等?我们一步步看。首先![]会是什么结果?我们之前说过,除了我们列的几种情况之外,其他值转成布尔值均为 true ,所以变成 [] == !true。即[] == false。是不是很熟悉,对,就是第一中情况,所以它也为 true 。
比较运算符
对于比较运算符,如果双方出现了非字符串,就根据 ToNumber 规则将双方转换为 Number 类型进行比较。如果有对象,会根据 ToPrimitive 得到基本类型再进行上述操作。所以最后要么是数字和数字进行比较,要么是字符串和字符串进行比较。我们看两个例子
console.log("32" < "042");//false
console.log(32 < "042");//true
第一个比较为字符串之间的比较,"3" 大于 "0",所以结果为false。而第二个会将"042"转换为 Number 类型,即32 < 42,结果为 true 。
我们再来看一个对象的例子
let x = {};
let y = {};
console.log(x<y);//false
console.log(x == y);//false
console.log(x>y);//false
console.log(x>=y);//true
是不是很奇怪,既不大于也不等于也不小于,但是大于等于却是 true。
首先,x,y都是对象,会根据 ToPrimitive 规则,返回 [object Object],很显然x不大于y也不小于y。那么为什么等于也不对?不要忘记,==是我们上节的内容,如果两个操作数都是对象,它们比较的是地址,而不会进行隐式转换。
那为什么 x>=y 会返回 true呢?实际上,x>=y 会被处理为 !(x<y),也就是 !false,现在,你能理解为什么最后一个会返回 true 了吧。
布尔值的隐式转换
以下几种情况会发生布尔值的强制转换
- if语句中的条件判断表达式
- for循环中的条件判断表达式
- while和do while循环重的条件判断表达式
- ? :中的条件判断表达式
在这几种情况下,会发生布尔值的隐式强制类型转换,规则就依据前面提到的 ToBoolean,即除掉下面这几种和假值对象之外,都转换为真值。
- undefined
- null
- false
- +0,-0,NaN
- ""
下面我们来看几个例子
console.log(true == {});//false
console.log(!!new Boolean(false));//true
console.log(!!"false");//true
我们先来解释以下!!操作会有什么结果。首先,!操作会把操作数强制转换为布尔值,随后取它的相反数。那么再加一个!就又把它变回去了。所以!!操作就是把一个非布尔值强制转换成了布尔值。
我们看到,即使是一个封装了假值,或值为 false 的字符串,转换后的结果也是 true 。因为他们不在我们上述转换为 false 的序列之内。
同时,对于第一个来说,乍一看,对象会变成 true ,true == true不是很合理么?还记得我们当初的内容吗?true首先会变成1,然后 {} 根据 ToPrimitive 规则会返回 [object Object],再将其转成 Number 变成 NaN。最后就变成了 1 == NaN。显然为 false 。
所以,当我们想用 x 来作判断条件时,千万不要使用 x == true,可以直接使用
if(x) {}
或者
if(!!x) {}
都是可以的。两者产生的效果是一样的,但是第二种看起来意义更加明确。
结语
隐式转换这个东西,各个开发者都对他们有着不同的意见。有的人认为它很不好用,因为这是自动的,不受我们控制。有的人觉得它很方便,能省不少功夫。但是不管怎样,我觉得我们都需要掌握它的具体细节。掌握了之后,如果你觉得它不好,你能够知道如何规避隐式转换。你觉得它好,你能够知道如何合理的运用它,达到自己想要的结果。
好了,这就是今天的全部内容了,希望看到这里的你给个关注和赞!