# JavaScript | 类型转换专题

先来一个表情包:

咳咳...这可能是每个 JSer 都绕不开的话题。


在第一篇笔记 JavaScript | 入门基础 中,我们简单介绍了原始类型的互相转换。

而在本篇中,我们将着重于讲述类型的自动转换机制并做易错点总结对比。

# 目录




# 原始类型转换

大多数情况下,运算符和函数会自动将赋予他们的值转换为正确的类型。比如:alert会自动将任何值都转换为字符串以进行显示,算术运算符会将值转换为数字。

而如果在某些情况下,我们想要将值显式地转换为我们期望的类型,可以手动地进行转换。(这部分见 JavaScript | 入门基础

手动转换是可控的,我们只要掌握正确的方式即可,往往不会出现大问题。但是自动转换在有些时候非常方便,但有些时候让我们更加头疼不已,这可能是 JavaScript 在设计之初的历史问题,也算是黑点之一。不过只要规范代码,记住规则,这玩意也没有那么难缠。

在 JavaScript 中,类型转换基本归为以下三种:

  • 转布尔值(boolean)
  • 转数字(number)
  • 转字符串(string)

我们分情况进行考虑:

# 转换为 boolean

对比之后要讲到的情况的复杂程度,转布尔值算是最简单的情况了。

毕竟它一般都符合直观上的感觉,都不用怎么记忆。

在条件判断(例如if)时,如果条件计算的结果不是boolean类型,会发生类型转换,将结果变为boolean类型。

除了undefinednullfalseNaN''0-0,其他所有值都转为true,包括所有的对象类型。

值得注意的是,if(value)if(value == true)并不是一种判断逻辑,得到的结果也可能会产生差别,如果有其他语言基础的同学很容易在这个问题上翻车车(详细的原因请继续往下看)。


# 比较运算符

JavaScript 的比较运算符可以分为两类。

一类是相等运算符

  • 相等(==
  • 不相等(!=
  • 严格/一致相等(===
  • 严格/一致不相等(!==

还有一类是关系运算符

  • 小于(<
  • 大于(>
  • 小于等于(<=
  • 大于等于(>=

比较的结果会返回truefalse

其中,只有===!==两种不会对比较的双方发生类型转换。

这也是为什么各大标准都推荐优先使用这两个作为判断是否相等的符号,除非是允许两个变量的类型互不相同的情况。

而其他的比较运算符,则会发生花里胡哨的转换。

# 相等运算符

对于使用会发生转换的比较运算符的变量xy,大致有着如下的判断规则(以==为例):

's rules.jpg

注:在这个图中,==两端的变量位置互换得到的结果是相同的。

可以看出,对于不同的两个类型做比较,最终落实到的往往大多都是“数值大小”的比较。

注意到:boolean类型做比较也会转成number类型。

所以很多时候if(value)能够true的变量到了if(value == true)的情况下反而返回false,往往就是因为value本身返回的值不是boolean类型。

if(value)中走的是Boolean(value)的自动转换规则,而在if(value == true)中走的是符合==规则的自动转换规则,自然结果也会不一样。

更多特殊知识点:

  • 对于nullundefined,不严格时可以互等,严格时只能自己和自己相等
  • NaN和任何值不相等,包括其自身
  • NaN和任何值比大小都为false
  • 正数零等于负数零
  • 对于对象之间的相等比较,会判断两个变量的引用是否相同(无论是否严格)。
  • numberstringboolean类型判断相等时都建议用严格相等(再次强调)

最后一个小问题:'42'String(42)new String(42)之间是否相等?

'42'(字符串)、String(42)(字符串)与new String(42)(对象),字符串和对象是不同的。

'42'=='42'(字符串相等)为true

new String(42) == new String(42)(对象相等)为false

# 关系运算符

关系运算符会比较两个操作数的大小:

  • 数字之间的比较当然就是数学意义上的比较
  • 字符串之间的比较遵循字符串比较算法,使用 Unicode 序列大小来判断(温习一下:'42' < '7'true
  • 而对于两个不同类型的操作数,如果是字符串与数字之间的比较,若字符串可以转换为number类型,则转换为number类型之间的比较,否则会返回false
  • boolean类型会转为number类型(但这真的有什么实际意义🐴?为了代码不规范的程序员预留后路?)

请使用关系运算符去比较相同类型之间的大小!(你清醒一点.jpg)

诸如为什么null <= 0null >= 0都是truenull == 0false的问题就不要问了

(网上的说法是:因为null <= 0是通过!(null > 0)来判断的,我未找到证据)


# 四则运算符

数学运算往往会将原始对象们自动转换为number类型,这看起来是“耍小聪明”的,不过这应该主要是因为网络与浏览器的环境下经常会有string等类型来表示数值的情况,所以这个自动转换在一定程度上简化了我们的代码量,也让我们不用在接收值时考虑它到底是string类型还是number类型。

不过,在四则运算符中,加法运算符+在二元运算中的与众不同是新手的第一个容易掉入的坑。

对于加法运算符的二元运算而言:

  • 运算中其中一方为字符串,那么就会把另一方也转换为字符串
  • 如果一方既不是数字也不是字符串,那么会将它根据自己的规则转换为数字或者字符串

大型迷惑现场:

1 + '1'     // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"

前两个较容易理解,而第三例4 + [1,2,3]涉及到对象向原始类型的转换规则,我放在下文 #对象向原始值转换 进行讲解。

再来个例子:

'a' + +'b' // "aNaN"

分析一下:

  1. 首先是+'b'想要将字符串'b'转换为number类型
  2. 因为'b'不是一个数字字符串,所以转换的结果为NaN(注意NaN也是属于number类型的定义范围,不要以为它是字符串)
  3. 然后就是一个string类型+number类型的表达式'a' + NaN
  4. 根据规则,将NaN转化为字符串'NaN',然后进行字符串拼接,得到'aNaN'

而其他的运算符就简单了,只要其中一方是数字,那么另外一方就会被转换为数字。

4 * '3'     // 12
4 * []      // 0
4 * [1, 2]  // NaN

# 对象向原始值转换

前面几个例子中有提到对象也参与运算符进行运算的情况,同样的,它们也是有着一定的转换规则的。

# ToPrimitive

对象到原始(Primitive)值的转换,是由许多内置函数和操作符自动调用内置的[[ToPrimitive]]函数,它会使用一个原始值作为返回值。

它有三种类型(或者说“暗示”):

  1. "string"

    当我们对期望一个字符串的对象执行操作时,如alert(),会发生从对象到字符串的转换:

    // 输出对象
    alert(obj);
    
    // 将对象作为属性键时
    something[obj] = 42;
    
  2. "number"

    当我们期待做数学运算时,发生对象到数值的转换:

    // 显式转换
    let num = Number(obj);
    
    // 数学运算(除了二元加法)
    let num = +obj; // 一元加法
    let delta = date1 - date2;
    
    // 小于/大于的比较
    let isGreater = date1 > date2;
    
  3. "default"

    在少数情况下,调用者不确定自己所期望的类型,这个情况下,绝大多数都会当作"number"类型来处理。

    // 二元加法
    let total = obj1 + obj2;
    
    // 比较运算符
    if (user == 1) { ... };
    

    规范明确描述了哪个操作符使用哪个暗示。极少数操作者虽然是内置的操作符,也“不知道自己期望什么”,并使用"default"暗示。

通常对于内置对象,"default"暗示的处理方式与"number"相同(除了Date对象,它会toString()),因此在实践中最后两个通常合并在一起。

为了进行转换,JavaScript 会按照以下顺序去执行调用对象的方法:

  1. 如果obj[Symbol.toPrimitive](hint)存在,则调用它。
  2. 否则,如果暗示是"string",尝试toString()valueOf()
  3. 否则,如果暗示是"number"或者"default",尝试valueOf()toString()
  4. 如果valueOf()转换后的值仍然不是原始类型,则会调用toString()

不论如何,[[ToPrimitive]]一定会保证对象转换后返回一个原始值,否则会报错。

我们可以返回去看看之前举的例子:

4 + [1, 2, 3] // "41,2,3"
4 * []        // 0
4 * [1, 2]    // NaN
  • 对于第一行
    1. [1, 2, 3]Array对象,在default暗示下调用了valueOf()方法,返回的仍然是对象,故不行
    2. 接着,调用[1, 2, 3]toString()方法,返回'1,2,3'字符串
    3. 数字4与字符串'1,2,3'相加,变成字符串相连,得到结果:'41,2,3'
  • 第二行
    1. []是一个空数组,和第一行例子类似地调用valueOf()不行后调用toString(),返回空字符串''
    2. 数字4与空字符串''相乘,空字符串''转换为数字0,结果为0
  • 第三行
    1. 同样的,推得数字4与字符串'1,2'相乘,'1,2'无法转为合法数字,于是变成NaN,相乘后仍然为NaN
    2. 可以发散地推理得,如果是单元素数组,结果就会变为数字与该元素对于的数字相乘

# 进一步转换

不过,经过转换后的原始值,当然是可以再发生进一步转换的。比如这里有一个自定义的对象:

let obj = {
  toString() {
    return "42";
  }
};

如果需要它转换,会调用toString()方法转换为初始值"42"

而这个时候如果上下文环境是数学运算,则字符串可以继续自动转换为数字:

alert(obj * 2);  // "42" * 2 => 42 * 2 => 84

# 结语

不知道看到这的你是否全部都看了一遍,或者看完了记住了多少(🐴?),但是记清这些规则对于了解 JavaScript 的特点有着一定的帮助(更大的帮助应该是面对稀奇古怪的面试题)。最起码弄清了这些,就不会被这些琐事打断代码的编写,去反复搜索不一定靠谱的解答,也不怕别人问起来时自己也一知半解,说个错误的答案出来。

像我一开始学习 JavaScript 时都是略读这些部分的内容,但学到后面才意识到这些琐碎的知识的重要之处,返回过来进行完整的总结。如果可以的话,最好还是能反复阅读这些知识点直到记住它们,或者自己也做一点相关的笔记(比心❤)


上次更新: 2020/2/19 18:14:38