JS类型转换

310 阅读7分钟

这里有一份简洁的前端知识体系等待你查收,看看吧,会有惊喜哦~如果觉得不错,恳求star哈~


类型转换

讲完了基本类型,我们来介绍一个现象:类型转换。


== 运算

因为JS是弱类型语言,所以类型转换发生非常频繁,大部分我们熟悉的运算都会先进行类型转换。大部分类型转换符合人类的直觉,“ == ”运算除外。因为试图实现跨类型的比较,“ == ”运算的规则复杂到几乎没人可以记住。

虽然==的行为很复杂,但是归根结底,类型不同的变量比较时 == 运算只有三条规则:

  1. undefined与null相等;
  2. 字符串和bool都转为数字再比较;
  3. 对象转换成基础类型再比较。

这样我们就可以理解一些不太符合直觉的例子了,比如:

false == '0' // true
true == 'true' // false
[] == 0 // true
[] == false // true
new Boolean('false') == false // false

这里不太符合直觉的有两点:

  1. 即使字符串与boolean比较,也都要转换成数字;
  2. 对象如果转换成了基础类型跟等号另一边类型恰好相同,则不需要转换成数字。

此外,== 的行为也经常跟if的行为(转换为boolean)混淆。总之,仅在确认 == 发生在Number和String类型之间时使用。

如果你觉得麻烦的话,这里提供了流程图:



下面看一个典型的例子:为什么[] == ![]?分析如下:

[] == ![]
↓
[] == false"" == false0 == false0 == 0

“ == ”运算属于设计失误,并非语言中有价值的部分,建议使用 === 比较。

幸好的是,实际上大部分类型转换规则是非常简单的,如下表所示:

Null Undefined Boolean(true) Boolean(false) Number String Symbol Object
Boolean FALSE FALSE -- -- 当数字为0或NaN时为false 当字符串为""时为false TRUE TRUE
Number 0 NaN 1 0 -- StringToNumber TypeError 拆箱操作
String "null" "undefined" "true" "false" NumberToString -- TypeError 拆箱操作
Object TypeError TypeError 装箱转换 装箱转换 装箱转换 装箱转换 装箱转换 --

从表格中不难发现,JS的类型转换,只有4种,分别是:

  1. 转换为布尔值
  2. 转换为数字
  3. 转换为字符串
  4. 转换为对象

其中,“转换为数字”跟“转换为字符串”主要发生在字符串跟数字之间。我们逐一分析。


转换为布尔值

在条件判断时,除了 undefined, null, false, NaN, '', 0, -0,其他所有值都转为 true,包括所有对象。


转换为数字

从上表不难发现,转换为数字的操作中,字符串到数字的情况比较特殊,所以重点介绍字符串到数字。

字符串到数字的类型转换,存在一个语法结构,类型转换支持十进制、二进制、八进制和十六进制,比如:

30
0b111
0o13
0xFF

此外,JavaScript支持的字符串语法还包括正负号科学计数法,可以使用大写或者小写的e来表示:

1e3
-1e-2

需要注意的是,parseInt 和 parseFloat 并不使用这个转换,所以支持的语法跟这里不尽相同。

在不传入第二个参数的情况下,parseInt只支持16进制前缀“0x”,而且会忽略非数字字符,也不支持科学计数法。建议传入parseInt的第二个参数。

parseFloat则直接把原字符串作为十进制来解析,它不会引入任何的其他进制。

多数情况下,Number 是比 parseInt 和 parseFloat 更好的选择。


转换为字符串

同上,转换为字符串的操作中,数字到字符串的情况比较特殊,所以重点介绍数字到字符串。

在较小的范围内,数字到字符串的转换是完全符合你直觉的十进制表示。当Number绝对值较大或者较小时,字符串表示则是使用科学计数法表示的。这是为了保证产生的字符串不会过长。


转换为对象

转换为对象包括两种操作:装箱转换跟拆箱转换。


装箱转换

每一种基本类型Number、String、Boolean、Symbol在对象中都有对应的类,所谓装箱转换,正是把基本类型转换为对应的对象,它是类型转换中一种相当重要的种类。

装箱机制会频繁产生临时对象,在一些对性能要求较高的场景下,我们应该尽量避免对基本类型做装箱转换。

使用内置的 Object 函数,我们可以在 JS 代码中显式调用装箱能力。

var symbolObject = Object(Symbol("a"));
console.log(typeof symbolObject); //object
console.log(symbolObject instanceof Symbol); //true
console.log(symbolObject.constructor == Symbol); //true

每一类装箱对象皆有私有的 Class 属性,这些属性可以用 Object.prototype.toString 获取:

var symbolObject = Object(Symbol("a"));
console.log(Object.prototype.toString.call(symbolObject)); //[object Symbol]

在 ES5 之前,没有任何方法可以更改私有的 Class 属性,因此Object.prototype.toString 是可以准确识别对象对应的基本类型的方法,它比 instanceof 更加准确。

但在 ES5 开始,[[class]] 私有属性被 Symbol.toStringTag 代替,详见这里


拆箱转换

在JavaScript标准中,规定了 ToPrimitive 函数,它是对象类型到基本类型的转换,即拆箱转换。

对象到 String 和 Number 的转换都遵循“先拆箱再转换”的规则。通过拆箱转换,把对象变成基本类型,再从基本类型转换为对应的 String 或者 Number。

拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果valueOf 和 toString都不存在,或者没有返回基本类型,则会产生类型错误 TypeError。

var o = {
  valueOf: () => {
    console.log ('valueOf');
    return {};
  },
  toString: () => {
    console.log ('toString');
    return {};
  },
};
o * 2;
// valueOf
// toString
// TypeError
// 先执行了valueOf,接下来是toString,最后抛出了一个TypeError,这就说明了这个拆箱转换失败了

到 String 的拆箱转换会优先调用 toString。我们把刚才的运算从o*2换成 String(o),那么你会看到调用顺序就变了。

var o = {
  valueOf: () => {
    console.log ('valueOf');
    return {};
  },
  toString: () => {
    console.log ('toString');
    return {};
  },
};
String (o);
// toString
// valueOf
// TypeError

在 ES6 之后,还允许对象通过显式指定 Symbol.toPrimitive 来覆盖原有的行为。

var o = {
  valueOf: () => {
    console.log ('valueOf');
    return {};
  },
  toString: () => {
    console.log ('toString');
    return {};
  },
};
o[Symbol.toPrimitive] = () => {
  console.log ('toPrimitive');
  return 'hello';
};
console.log (o + '');
// toPrimitive
// hello

其他类型转换场景

四则运算符

加法运算符不同于其他几个运算符,它有以下两个特点:

  1. 如果一方不是字符串或者数字,先将它转换为数字或者字符串
  2. 运算中其中一方为字符串,那么就会把另一方也转换为字符串
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"

如果你对于答案有疑问的话,请看解析:

  1. 对于第一行代码来说,触发特点2,所以将数字 1 转换为字符串,得到结果 '11'
  2. 对于第二行代码来说,触发特点1,所以将 true 转为数字 1
  3. 对于第三行代码来说,触发特点1,所以将数组通过 toString 转为字符串 1,2,3,得到结果 41,2,3

另外对于加法还需要注意这个表达式 'a' + + 'b'

'a' + + 'b' // -> "aNaN"

因为 + 'b' 等于 NaN,所以结果为 "aNaN",你可能也会在一些代码中看到过 + '1' 的形式来快速获取 number 类型。

那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字。

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

比较运算符

  1. 如果是对象,先进行拆箱操作。
  2. 如果是字符串,就通过 unicode 字符索引来比较
let a = {
  valueOf() {
    return 0
  },
  toString() {
    return '1'
  }
}
a > -1 // true

在以上代码中,因为 a 是对象,所以会通过 valueOf 转换为原始类型再比较值。