JS扫盲之隐式转换

251 阅读10分钟

隐式转换可谓是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) {}

都是可以的。两者产生的效果是一样的,但是第二种看起来意义更加明确。

结语

隐式转换这个东西,各个开发者都对他们有着不同的意见。有的人认为它很不好用,因为这是自动的,不受我们控制。有的人觉得它很方便,能省不少功夫。但是不管怎样,我觉得我们都需要掌握它的具体细节。掌握了之后,如果你觉得它不好,你能够知道如何规避隐式转换。你觉得它好,你能够知道如何合理的运用它,达到自己想要的结果。

好了,这就是今天的全部内容了,希望看到这里的你给个关注和赞!