JavaScript 中的类型转换

244 阅读15分钟

JavaScript 中的类型转换

前言

将值从一种类型转换为另一种类型通常称为类型转换。

ES6 前,JavaScript 共有六种数据类型:Undefined、Null、Boolean、Number、String、Object。

我们先捋一捋简单类型之间的转换。

简单类型转布尔

不论我们使用 Boolean 函数显式转换还是 if 等隐式转换,在 JavaScript 中,只有 6 种值可以被转换成 false,其他都会被转换成 true。

//注意,当 Boolean 函数不传任何参数时,会返回 false。
console.log(Boolean()) // false

console.log(Boolean(false)) // false

console.log(Boolean(undefined)) // false
console.log(Boolean(null)) // false
console.log(Boolean(+0)) // false
console.log(Boolean(-0)) // false
console.log(Boolean(NaN)) // false
console.log(Boolean("")) // false

简单类型转数字

我们可以使用 Number,parseInt,parseFloat 函数将类型转换成数字类型,如果参数无法被转换为数字,则返回 NaN。

在看例子之前,我们先看ES5 规范 15.7.1.1 中关于 Number 的介绍:

图片alt
如果 Number 函数不传参数,返回 +0,如果有参数,调用 ToNumber(value)
ToNumber 表示的是一个底层规范实现上的方法,并没有直接暴露出来。
ToNumber 则直接给了一个对应的结果表。表如下:

参数类型结果
UndefinedNaN
Null+0
Boolean如果参数是 true,返回 1。参数为 false,返回 +0
Number返回与之相等的值
String这段比较复杂,看例子

让我们写几个例子验证一下:

console.log(Number()) // +0

console.log(Number(undefined)) // NaN
console.log(Number(null)) // +0 比较特殊

console.log(Number(false)) // +0
console.log(Number(true)) // 1

console.log(Number("123")) // 123
console.log(Number("-123")) // -123
console.log(Number("1.2")) // 1.2
console.log(Number("000123")) // 123
console.log(Number("-000123")) // -123

console.log(Number("0x11")) // 17

console.log(Number("")) // 0
console.log(Number(" ")) // 0

console.log(Number("123 123")) // NaN
console.log(Number("foo")) // NaN
console.log(Number("100a")) // NaN

如果通过 Number 转换函数传入一个字符串,它会试图将其转换成一个整数或浮点数,而且会忽略所有前导的 0,如果有一个字符不是数字,结果都会返回 NaN,鉴于这种严格的判断,我们一般还会使用更加灵活的 parseInt 和 parseFloat 进行转换。

parseInt 只解析整数,parseFloat 则可以解析整数和浮点数,他们的入参一般都是string类型,如果你传入一个非string,你所传入的值首先将自动地被强制转换为一个string。parseInt还有第二个参数来指定使用哪种进制(也叫基数)来翻译数字的string内容,如果你不传递第二个参数parseInt将会根据开头的字符进行猜测。如字符串前缀是 "0x" 或者"0X",parseInt 将其解释为十六进制数。parseInt 和 parseFloat 都会跳过任意数量的前导空格,尽可能解析更多数值字符,并忽略后面的内容。如果第一个非空格字符是非法的数字直接量,将最终返回 NaN:

console.log(parseInt("3 abc")) // 3
console.log(parseInt("3 abc",10)) // 3
console.log(parseFloat("3.14 abc")) // 3.14
console.log(parseInt("-12.34")) // -12
console.log(parseInt("0xFF")) // 255
console.log(parseFloat(".1")) // 0.1
console.log(parseInt("0.1")) // 0

简单类型转字符

我们使用 String 函数 或 toString方法(下面有具体介绍) 将类型转换成字符串类型,所有的对象除了 null 和 undefined 之外的任何值都具有 toString 方法,通常情况下,它和使用 String 方法返回的结果一致。依然先看 规范15.5.1.1中有关 String 函数的介绍:

图片alt
如果 String 函数不传参数,返回空字符串,如果有参数,调用 ToString(value),而 ToString也是一个底层规范实现上的方法,并没有直接暴露出来。 下面给了一个对应的结果表。表如下

参数类型结果
Undefined"undefined"
Null"null"
Boolean如果参数是 true,返回 "true"。参数为 false,返回 "false"
Number又是比较复杂,可以看例子
String返回与之相等的值

让我们写几个例子验证一下:

console.log(String()) // 空字符串

console.log(String(undefined)) // undefined
console.log(String(null)) // null

console.log(String(false)) // false
console.log(String(true)) // true

console.log(String(0)) // 0
console.log(String(-0)) // 0
console.log(String(NaN)) // NaN
console.log(String(Infinity)) // Infinity
console.log(String(-Infinity)) // -Infinity
console.log(String(1)) // 1

下面我们看下简单类型和复杂类型之间的转换:

简单类型转对象

简单类型到对象的转换非常简单,原始值通过调用 new String()、new Number() 或者 new Boolean() 构造函数,转换为它们各自的包装对象。

null 和 undefined 属于例外,当将它们用在期望是一个对象的地方都会造成一个类型错误 (TypeError) 异常,而不会执行正常的转换

var a = 1;
console.log(typeof a); // number
var b = new Number(a);
console.log(typeof b); // object

对象转布尔值

对象到布尔值的转换非常简单:所有对象(包括数组和函数)都转换为 true。对于包装对象也是这样,举个例子:

console.log(Boolean(new Boolean(false))) // true

对象转字符串和数字(重点)

对象到字符串和对象到数字的转换都是通过调用待转换对象的一个方法来完成的。而 JavaScript 对象有两个不同的方法来执行转换,一个是 toString,一个是 valueOf。注意这个跟上面所说的 ToString 和 ToNumber 是不同的,这两个方法是真实暴露出来的方法。
所有的对象除了 null 和 undefined 之外的任何值都具有 toString 方法,通常情况下,它和使用 String 方法返回的结果一致。toString 方法的作用在于返回一个反映这个对象的字符串,然而这才是情况复杂的开始。

JavaScript 下的很多类型根据各自的特点,定义了不同版本的 toString 方法。例如:

  1. object 的 toString 方法会根据这个对象的[[class]]内部属性,返回由 "[object " 和 class 和 "]" 三个部分组成的字符串。mdn地址
  2. 数组的 toString 方法将每个数组元素转换成一个字符串,并在元素之间添加逗号后合并成结果字符串,如果有null,undefined,则对应位置为空。false 转为 ‘false’
  3. 函数的 toString 方法返回源代码字符串。
  4. 日期的 toString 方法返回一个可读的日期和时间字符串。
  5. RegExp 的 toString 方法返回一个表示正则表达式直接量的字符串。
  6. 简单类型返回同String()

我们看下面例子:

console.log(({}).toString()) // [object Object]

console.log([].toString()) // ""
console.log([0].toString()) // "0"
console.log([1, 2, 3].toString()) // "1,2,3"
console.log([1,null,undefined,false].toString()) // "1,,,false"
console.log([{}].toString()) // "[object Object]"
console.log((function(){var a = 1;}).toString()) // "function (){var a = 1;}"
console.log((/\d+/g).toString()) // /\d+/g
console.log((new Date(2010, 0, 1)).toString()) // "Fri Jan 01 2010 00:00:00 GMT+0800 (CST)"
console.log(true.toString()) // "true"

而另一个转换对象的函数是 valueOf,表示对象的原始值。默认的 valueOf 方法返回这个对象本身,数组、函数、正则简单的继承了这个默认方法,也会返回对象本身。日期是一个例外,它会返回它的一个内容表示: 1970 年 1 月 1 日以来的毫秒数。
列子如下:

console.log('string'.valueOf()); // 'string'
console.log(true.valueOf()); // true
console.log({}.valueOf()); // {}
console.log(function a() {}.valueOf()); // function a() {}

var date = new Date(2017, 4, 21);
console.log(date.valueOf()) // 1495296000000

注: 当对象上有自定义的toString 或 valueOf 时,当通过 String() 或 Number()转换时会优先调用自定义的方法,如果没有就会调用原型链上默认方法。

const obj = {
        a: 1,
        toString: () => "this is myString",
        valueOf: () => "666",
      };
console.log(String(obj)); // this is myString
console.log(Number(obj)); // 666

对象接着转字符串和数字

了解了 toString 方法和 valueOf 方法,我们分析下从对象到字符串是如何转换的。看规范 ES5 9.8, 这次我们加上 Object 的转换规则。

对象转字符串:

参数类型结果
Object1. primValue = ToPrimitive(input, String) 2. 返回 ToString(primValue)

所谓的 ToPrimitive 方法,其实就是输入一个值,然后返回一个一定是基本类型的值。

我们总结一下,当我们用 String 方法转化一个值的时候,如果是基本类型,就参照 “简单类型转字符” 这一节的对应表,如果不是基本类型,我们会将调用一个 ToPrimitive 方法,将其转为基本类型,然后再参照“简单类型转字符” 这一节的对应表进行转换。

其实,从对象到数字的转换也是一样:

参数类型结果
Object1. primValue = ToPrimitive(input, Number) 2. 返回ToNumber(primValue)

虽然转换成基本值都会使用 ToPrimitive 方法,但传参有不同,最后的处理也有不同。

ToPrimitive

那接下来就要看看 ToPrimitive 了,在了解了 toString 和 valueOf 方法后,这个也很简单。

让我们看规范,规范 ES5 9.1函数语法表示如下:

ToPrimitive(input[, PreferredType])

第一个参数是 input,表示要处理的输入值。

第二个参数是 PreferredType,非必填,表示希望转换成的类型,有两个值可以选,Number 或者 String。

当不传入 PreferredType 时,如果 input 是日期类型,相当于传入 String,否则,都相当于传入 Number。

如果传入的 input 是 Undefined、Null、Boolean、Number、String 简单类型,直接返回该值。

如果是 ToPrimitive(obj, Number),处理步骤如下:

  1. 如果 obj 为基本类型,直接返回
  2. 调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
  3. 否则,调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。
  4. 否则,JavaScript 抛出一个类型错误异常。

如果是 ToPrimitive(obj, String),处理步骤如下:

  1. 如果 obj 为基本类型,直接返回
  2. 调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。
  3. 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
  4. 否则,JavaScript 抛出一个类型错误异常。

对象转字符串

所以总结下,对象转字符串可以概括为:

  1. 如果对象具有 toString 方法,则调用这个方法。如果他返回一个原始值,JavaScript 将这个值转换为字符串,并返回这个字符串结果。
  2. 如果对象没有 toString 方法,或者这个方法并不返回一个原始值,那么 JavaScript 会调用 valueOf 方法。如果存在这个方法,则 JavaScript 调用它。如果返回值是原始值,JavaScript 将这个值转换为字符串,并返回这个字符串的结果。
  3. 否则,JavaScript 无法从 toString 或者 valueOf 获得一个原始值,这时它将抛出一个类型错误异常。
console.log(String({ a: 1 })); // [object Object]
console.log(String([])); // ''
console.log(String([1,2,3])); // '1,2,3'

对象转数字

对象转数字的过程中,JavaScript 做了同样的事情,只是它会首先尝试 valueOf 方法

如果对象具有 valueOf 方法,且返回一个原始值,则 JavaScript 将这个原始值转换为数字并返回这个数字 否则,如果对象具有 toString 方法,且返回一个原始值,则 JavaScript 将其转换并返回。 否则,JavaScript 抛出一个类型错误异常。

console.log(Number({})) // NaN
console.log(Number({a : 1})) // NaN

console.log(Number([])) // 0
console.log(Number([0])) // 0
console.log(Number([1, 2, 3])) // NaN
console.log(Number(function(){var a = 1;})) // NaN
console.log(Number(/\d+/g)) // NaN
console.log(Number(new Date(2010, 0, 1))) // 1262275200000
console.log(Number(new Error('a'))) // NaN

注意,在这个例子中,[] 和 [0] 都返回了 0,而 [1, 2, 3] 却返回了一个 NaN。我们分析一下原因:

当我们 Number([]) 的时候,先调用 [] 的 valueOf 方法,此时返回 [],因为返回了一个对象而不是原始值,所以又调用了 toString 方法,此时返回一个空字符串,接下来调用 ToNumber 这个规范上的方法,参照对应表,转换为 0, 所以最后的结果为 0。

而当我们 Number([1, 2, 3]) 的时候,先调用 [1, 2, 3] 的 valueOf 方法,此时返回 [1, 2, 3],再调用 toString 方法,此时返回 1,2,3,接下来调用 ToNumber,参照对应表,因为无法转换为数字,所以最后的结果为 NaN。

关于 ‘’+‘’ 转换

一元操作符 +

console.log(+'1');

当 + 运算符作为一元操作符的时候,查看 ES5规范11.4.6,会调用 ToNumber 处理该值,相当于 Number('1'),最终结果返回数字 1。

那么下面的这些结果呢?

console.log(+[]);
console.log(+['1']);
console.log(+['1', '2', '3']);
console.log(+{});

当输入的值是对象的时候,就会使用上面讲过的对象转数字。

参数类型结果
Object1. primValue = ToPrimitive(input, Number) 2. 返回ToNumber(primValue)

先调用 ToPrimitive(input, Number) 方法,执行的步骤是:

  1. 如果 obj 为基本类型,直接返回
  2. 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
  3. 否则,调用 toString 方法,如果返回一个原始值,则JavaScript 将其返回。
  4. 否则,JavaScript 抛出一个类型错误异常。

以 +[] 为例,[] 调用 valueOf 方法,返回一个空数组,因为不是原始值,调用 toString 方法,返回 ""。

得到返回值后,然后再调用 ToNumber 方法,"" 对应的返回值是 0,所以最终返回 0。

剩下的例子以此类推。结果是:

console.log(+['1']); // 1
console.log(+['1', '2', '3']); // NaN
console.log(+{}); // NaN

二元操作符 +

console.log(1 + '1');

现在 + 运算符又变成了二元操作符,毕竟它也是加减乘除中的加号

1 + '1' 我们知道答案是 '11',那 null + 1、[] + []、[] + {}、{} + {} 呢?

如果要了解这些运算的结果,不可避免的我们要从规范下手ES5规范11.6.1
到底当执行 + 运算的时候,会执行怎样的步骤呢?让我们根据规范11.6.1 来捋一捋:

当计算 value1 + value2时:

  1. lprim = ToPrimitive(value1) // 第二个参数默认是Number,除非是日期类型
  2. rprim = ToPrimitive(value2)
  3. 如果 lprim 是字符串或者 rprim 是字符串,那么返回 ToString(lprim) 和 ToString(rprim)的拼接结果
  4. 返回 ToNumber(lprim) 和 ToNumber(rprim)的运算结果

让我们来举几个例子:

console.log(null + 1);

按照规范的步骤进行分析:

  1. lprim = ToPrimitive(null) 因为null是基本类型,直接返回,所以 lprim = null
  2. rprim = ToPrimitive(1) 因为 1 是基本类型,直接返回,所以 rprim = 1
  3. lprim 和 rprim 都不是字符串
  4. 返回 ToNumber(null) 和 ToNumber(1) 的运算结果 接下来:

ToNumber(null) 的结果为0,(上面讲过 Number(null)),ToNumber(1) 的结果为 1)

所以,null + 1 相当于 0 + 1,最终的结果为数字 1。

这个还算简单,看些稍微复杂的:

console.log([] + []);

依然按照规范:

  1. lprim = ToPrimitive([]),[]是数组,相当于ToPrimitive([], Number),先调用valueOf方法,返回对象本身,因为不是原始值,调用toString方法,返回空字符串""
  2. rprim类似。
  3. lprim和rprim都是字符串,执行拼接操作 所以,[] + []相当于 "" + "",最终的结果是空字符串""。

看个更复杂的:

// 两者结果一致
console.log([] + {});
console.log({} + []);

按照规范:

  1. lprim = ToPrimitive([]),lprim = ""
  2. rprim = ToPrimitive({}),相当于调用 ToPrimitive({}, Number),先调用 valueOf 方法,返回对象本身,因为不是原始值,调用 toString 方法,返回 "[object Object]"
  3. lprim 和 rprim 都是字符串,执行拼接操作 所以,[] + {} 相当于 "" + "[object Object]",最终的结果是 "[object Object]"。

下面的例子,可以按照示例类推出结果:

console.log(1 + true);
console.log({} + {});
console.log(new Date(2017, 04, 21) + 1) // 这个知道是数字还是字符串类型就行

结果是:

console.log(1 + true); // 2
console.log({} + {}); // "[object Object][object Object]"
console.log(new Date(2017, 04, 21) + 1) // "Sun May 21 2017 00:00:00 GMT+0800 (CST)1"

== 相等

"==" 用于比较两个值是否相等,当要比较的两个值类型不一样的时候,就会发生类型的转换。

关于使用"=="进行比较的时候,具体步骤可以查看规范11.9.5

1625022114269_1E8C1F35-5A8D-4204-8242-315FCF209F16.png

觉得看规范判断太复杂?我们来分几种情况来看:

1. null和undefined

console.log(null == undefined);

看规范第2、3步:

 2. x是null并且y是undefined,返回true  

 3. x是undefined并且y是null,返回true

所以例子的结果自然为 true。

2. 字符串与数字

console.log('1' == 1);

结果肯定是true,问题在于是字符串转化成了数字和数字比较还是数字转换成了字符串和字符串比较呢?
看规范第4、5步:

4.x是数字,y是字符串,判断x == ToNumber(y)

5.x是字符串,y是数字,判断ToNumber(x) == y

结果很明显,都是转换成数字后再进行比较

3. 布尔值和其他类型

console.log(true == '2')

当要判断的一方出现 false 的时候,往往最容易出错,比如上面这个例子,凭直觉应该是 true,毕竟 Boolean('2') 的结果可是true,但这道题的结果却是false。

归根到底,还是要看规范,规范第6、7步:

6.x是布尔值,判断ToNumber(x) == y

7.y是布尔值,判断x ==ToNumber(y)

当一方出现布尔值的时候,就会对这一方的值进行ToNumber处理,也就是说true会被转化成1,

true == '2' 就相当于 1 == '2' 就相当于 1 == 2,结果自然是 false。

所以当一方是布尔值的时候,会对布尔值进行转换,因为这种特性,所以尽量少使用 xx == true 和 xx == false 的写法。

比如:

// 不建议
if (a == true) {}

// 建议
if (a) {}
// 更好
if (!!a) {}

4. 对象与非对象

console.log( 42 == ['42'])

看规范第8、9步:

8. x是字符串或者数字,y是对象,判断x == ToPrimitive(y)
9. x是对象,y是字符串或者数字,判断ToPrimitive(x) == y

以这个例子为例,会使用 ToPrimitive 处理 ['42'],调用valueOf,返回对象本身,再调用 toString,返回 '42',所以

42 == ['42'] 相当于 42 == '42' 相当于42 == 42,结果为 true。

到此为止,我们已经看完了第2、3、4、5、6、7、8、9步,其他的一概返回 false。

其他

再多举几个例子进行分析:

console.log(false == undefined)
// false == undefined 相当于 0 == undefined 不符合上面的情形,执行最后一步 返回 false 
console.log(false == [])
// false == [] 相当于 0 == [] 相当于 0 == '' 相当于 0 == 0,结果返回 true  
console.log([] == ![])
// 首先会执行 ![] 操作,转换成 false,相当于 [] == false 相当于 [] == 0 相当于 '' == 0 相当于 0 == 0,结果返回 true

最后再举一些会让人踩坑的例子:

console.log(false == "0")
console.log(false == 0)
console.log(false == "")

console.log("" == 0)
console.log("" == [])

console.log([] == 0)

console.log("" == [null])
console.log(0 == "\n")  // Number("\n"), Number("\r"),Number("\t") 都是 0
console.log([] == 0)

以上均返回 true

其实还有很多情形会发生隐式类型转换,比如if、? :、&&等情况,但相对来说,比较简单,就不再讲解。

参考文章:github.com/mqyqingfeng…