Skip to content

前言

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

我们先捋一捋基本类型之间的转换。

原始值转布尔

我们使用 Boolean 函数将类型转换成布尔类型,在 JavaScript 中,只有 6 种值可以被转换成 false,其他都会被转换成 true。

js
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

注意,当 Boolean 函数不传任何参数时,会返回 false。

原始值转数字

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

根据规范,如果 Number 函数不传参数,返回 +0,如果有参数,调用 ToNumber(value)。

注意这个 ToNumber 表示的是一个底层规范实现上的方法,并没有直接暴露出来。

而 ToNumber 则直接给了一个对应的结果表。表如下:

参数类型结果
UndefinedNaN
Null+0
Boolean如果参数是 true,返回 1。参数为 false,返回 +0
Number返回与之相等的值

String 这段比较复杂,看例子 让我们写几个例子验证一下:

js
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 则可以解析整数和浮点数,如果字符串前缀是 "0x" 或者"0X",parseInt 将其解释为十六进制数,parseInt 和 parseFloat 都会跳过任意数量的前导空格,尽可能解析更多数值字符,并忽略后面的内容。如果第一个非空格字符是非法的数字直接量,将最终返回 NaN:

js
console.log(parseInt("3 abc")) // 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 函数将类型转换成字符串类型,依然先看 规范15.5.1.1中有关 String 函数的介绍:

如果 String 函数不传参数,返回空字符串,如果有参数,调用 ToString(value),而 ToString 也给了一个对应的结果表。表如下:

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

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

js
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

注意这里的 ToString 和上一节的 ToNumber 都是底层规范实现的方法,并没有直接暴露出来。

原始值转对象

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

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

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

对象转布尔值

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

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

对象转字符串和数字

对象到字符串和对象到数字的转换都是通过调用待转换对象的一个方法来完成的。而 JavaScript 对象有两个不同的方法来执行转换,一个是 toString,一个是 valueOf。注意这个跟上面所说的 ToString 和 ToNumber 是不同的,这两个方法是真实暴露出来的方法。

所有的对象除了 null 和 undefined 之外的任何值都具有 toString 方法,通常情况下,它和使用 String 方法返回的结果一致。toString 方法的作用在于返回一个反映这个对象的字符串,然而这才是情况复杂的开始。

在《JavaScript专题之类型判断(上)》中讲到过 Object.prototype.toString 方法会根据这个对象的[[class]]内部属性,返回由 "[object " 和 class 和 "]" 三个部分组成的字符串。举个例子:

js
Object.prototype.toString.call({a: 1}) // "[object Object]"
({a: 1}).toString() // "[object Object]"
({a: 1}).toString === Object.prototype.toString // true

我们可以看出当调用对象的 toString 方法时,其实调用的是 Object.prototype 上的 toString 方法。

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

  • 数组的 toString 方法将每个数组元素转换成一个字符串,并在元素之间添加逗号后合并成结果字符串。
  • 函数的 toString 方法返回源代码字符串。
  • 日期的 toString 方法返回一个可读的日期和时间字符串。
  • RegExp 的 toString 方法返回一个表示正则表达式直接量的字符串。
js
console.log(({}).toString()) // [object Object]

console.log([].toString()) // ""
console.log([0].toString()) // 0
console.log([1, 2, 3].toString()) // 1,2,3
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)

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

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

对象接着转字符串和数字

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

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

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

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

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

虽然转换成基本值都会使用 ToPrimitive 方法,但传参有不同,最后的处理也有不同,转字符串调用的是 ToString,转数字调用 ToNumber。

ToPrimitive

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

ToPrimitive(input[, PreferredType]) 第一个参数是 input,表示要处理的输入值。

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

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

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

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

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

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

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

对象转字符串

所以总结下,对象转字符串(就是 Number() 函数)可以概括为:

  • 如果对象具有 toString 方法,则调用这个方法。如果他返回一个原始值,JavaScript 将这个值转换为字符串,并返回这个字符串结果。
  • 如果对象没有 toString 方法,或者这个方法并不返回一个原始值,那么 JavaScript 会调用 valueOf 方法。如果存在这个方法,则 JavaScript 调用它。如果返回值是原始值,JavaScript 将这个值转换为字符串,并返回这个字符串的结果。
  • 否则,JavaScript 无法从 toString 或者 valueOf 获得一个原始值,这时它将抛出一个类型错误异常。

对象转数字

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

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

举个例子:

js
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。

JSON.stringify

值得一提的是:JSON.stringify() 方法可以将一个 JavaScript 值转换为一个 JSON 字符串,实现上也是调用了 toString 方法,也算是一种类型转换的方法。下面讲一讲JSON.stringify 的注意要点:

1.处理基本类型时,与使用toString基本相同,结果都是字符串,除了 undefined

js
console.log(JSON.stringify(null)) // null
console.log(JSON.stringify(undefined)) // undefined,注意这个undefined不是字符串的undefined
console.log(JSON.stringify(true)) // true
console.log(JSON.stringify(42)) // 42
console.log(JSON.stringify("42")) // "42"

2.布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。

JSON.stringify([new Number(1), new String("false"), new Boolean(false)]); // "[1,"false",false]"

3.undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。

js
JSON.stringify({x: undefined, y: Object, z: Symbol("")}); 
// "{}"

JSON.stringify([undefined, Object, Symbol("")]);          
// "[null,null,null]"

4.JSON.stringify 有第二个参数 replacer,它可以是数组或者函数,用来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除。

js
function replacer(key, value) {
  if (typeof value === "string") {
    return undefined;
  }
  return value;
}

var foo = {foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7};
var jsonString = JSON.stringify(foo, replacer);

console.log(jsonString)
// {"week":45,"month":7}
var foo = {foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7};
console.log(JSON.stringify(foo, ['week', 'month']));
// {"week":45,"month":7}

5.如果一个被序列化的对象拥有 toJSON 方法,那么该 toJSON 方法就会覆盖该对象默认的序列化行为:不是那个对象被序列化,而是调用 toJSON 方法后的返回值会被序列化,例如:

js
var obj = {
  foo: 'foo',
  toJSON: function () {
    return 'bar';
  }
};
JSON.stringify(obj);      // '"bar"'
JSON.stringify({x: obj}); // '{"x":"bar"}'

一元操作符 +

console.log(+'1');

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

那么下面的这些结果呢?

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

既然是调用 ToNumber 方法,当输入的值是对象的时候,先调用 ToPrimitive(input, Number) 方法,执行的步骤是:

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

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

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

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

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

二元操作符 +

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

当计算 value1 + value2时:

  • lprim = ToPrimitive(value1)
  • rprim = ToPrimitive(value2)
  • 如果 lprim 是字符串或者 rprim 是字符串,那么返回 ToString(lprim) 和 ToString(rprim)的拼接结果
  • 返回 ToNumber(lprim) 和 ToNumber(rprim)的运算结果

让我们来举几个例子:

1.Null 与数字

console.log(null + 1); 按照规范的步骤进行分析:

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

接下来:

ToNumber(null) 的结果为0,ToNumber(1) 的结果为 1

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

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

2.数组与数组

console.log([] + []);

依然按照规范:

  • lprim = ToPrimitive([]),[]是数组,相当于ToPrimitive([], Number),先调用valueOf方法,返回对象本身,因为不是原始值,调用toString方法,返回空字符串""
  • rprim类似。
  • lprim和rprim都是字符串,执行拼接操作

所以,[] + []相当于 "" + "",最终的结果是空字符串""。

看个更复杂的:

3.数组与对象

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

按照规范:

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

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

js
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"

注意 以上的运算都是在 console.log 中进行,如果你直接在 Chrome 或者 Firebug 开发工具中的命令行直接输入,你也许会惊讶的看到一些结果的不同,比如:

js
{}+[]
// 0
js
({}+[])
// "[object Object]"

其实,在不加括号的时候,{} 被当成了一个独立的空代码块,所以 {} + [] 变成了 +[],结果就变成了 0

同样的问题还出现在 {} + {} 上,而且火狐和谷歌的结果还不一样:

js
 {} + {}
// 火狐: NaN
// 谷歌: "[object Object][object Object]"

如果 {} 被当成一个独立的代码块,那么这句话相当于 +{},相当于 Number({}),结果自然是 NaN,可是 Chrome 却在这里返回了正确的值。

那为什么这里就返回了正确的值呢?我也不知道,欢迎解答~

== 相等

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

当执行x == y 时:

  1. 如果x与y是同一类型:

    • x是Undefined,返回true
    • x是Null,返回true
    • x是数字:
      • x是NaN,返回false
      • y是NaN,返回false
      • x与y相等,返回true
      • x是+0,y是-0,返回true
      • x是-0,y是+0,返回true
      • 返回false
    • x是字符串,完全相等返回true,否则返回false
    • x是布尔值,x和y都是true或者false,返回true,否则返回false
    • x和y指向同一个对象,返回true,否则返回false
  2. x是null并且y是undefined,返回true

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

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

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

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

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

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

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

  10. 返回false

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

null和undefined

console.log(null == undefined);

  • x是null并且y是undefined,返回true
  • x是undefined并且y是null,返回true

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

这时候编写判断对象的类型 type 函数时,如果输入值是 undefined,就返回字符串 undefined,如果是 null,就返回字符串 null。

如果是你,你会怎么写呢?

下面是 jQuery 的写法:

js
function type(obj) {
    if (obj == null) {
        return obj + '';
    }
    ...
}

字符串与数字

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

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

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

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

布尔值和其他类型

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

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

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

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

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

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

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

比如:

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

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

对象与非对象

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

  • x不是字符串或者数字,y是对象,判断x == ToPrimitive(y)
  • x是对象,y不是字符串或者数字,判断ToPrimitive(x) == y 以这个例子为例,会使用 ToPrimitive 处理 ['42'],调用valueOf,返回对象本身,再调用 toString,返回 '42',所以

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

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

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

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

js
console.log(false == "0") //true
console.log(false == 0) //true
console.log(false == "") //true

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

console.log([] == 0)

console.log("" == [null])
console.log(0 == "\n")
console.log([] == 0)

以上均返回 true