最惊险,最刺激,最摸不着头脑的一章节 - 强制类型转换。
从JavaScript出生开始,这个特性就一直饱受争议。喜欢用的人爱的要死,不喜欢的人恨不得把创始团队给干了。这也是经常出现在各大面试题库中的经典类型问题了。
首先要清楚,什么是强制类型转换。

一 类型转换

将一种类型的值转换成另一种类型的值通常称为 类型转换,这是显示情况下的
当转换是在隐式下时候,就称为 强制类型转换 。

  1. var a = 42;
  2. a + ""; // "42" 隐式强制类型转换
  3. String(a); // "42" 显式强制类型转换

对第一条表达式来说,这里 + 运算符其中一个操作值是字符串,所以这里将 a 转换成了字符串然后进行拼接。
而对于第二条表达式,明显调用了JavaScript内置的原生函数String()对a进行了类型转换。
当然第一种情况,要是身经百战的JavaScript开发者应该是一眼就能看出来这里要进行强制类型转换,所以某种意义上来说也算是显式强制类型转换。反之,你要是不知道String原生函数,那第二条对你来说是隐式的。
但其中隐藏或显示的副作用对开发者理解起来是有一定的影响,故日常使用中需要考虑大部分开发者的情况再使用。

二 抽象操作

这一段着重讲解一下JavaScript自己带的几个转换类型的抽象操作:

  • toString
  • toNumber
  • toBoolean

ToString

字面意义上就是将非字符串转成字符串的强制类型转换的抽象方法。

  1. (42).toString(); // "42"
  2. (true).toString(); // "true"
  3. (1000000000000000000000).toString(); // "1e21" 数字转换还是遵守通用的规则
  4. [1,2,3].toString(); // "1,2,3" 数组则会默认用,将元素连接起来

null和undefined是无法使用toString()方法,因为它们只有原始值不会生产包装类,所以无法调用Object.prototype.toString方法。
如果操作的是对象object,则返回值为

  1. ({}).toString(); // "[object Object]"

如果对象里自定义了 toString() 方法,则返回定义的 toString() 方法的返回值。

  1. ({ toString: function() { return "object" } }).toString(); // "object"

这里也讲一下日常开发种经常用到的JSON方法:JSON.stringify(…)。
该方法主要是是将JSON对象转换成为字符串。但相对于大部分的简单值来说,其效果和toString()相同

  1. JSON.stringify(42); // "42"
  2. JSON.stringify("42"); // ""42""
  3. JSON.stringify(true); // "true"
  4. JSON.stringify(null); // "null"

对对于一些不安全的JSON值,其会做出忽略,比如:undefined,function,symbol和包含循环引用的对象。

  1. JSON.stringify(undefined); // undefined
  2. JSON.stringify(function() {}); // undefined
  3. JSON.stringify([1,function() {},3]); // "[1,null,3]" 这里JSON会自动加上null保证数组位数正确
  4. JSON.stringify({ a:1, b: function() {} }); // "{"a":1}"

如果有包含循环引用的对象的时候,方法会直接报错
如果对象中自定义了toJSON方法,则JSON.stringify运行前会先调用toJSON方法来获取返回值

  1. JSON.stringify({}); // "{}"
  2. JSON.stringify({ toJSON: function() { return 1 } }); // "1"

JSON.stringify其实还包含两个经常容易被忽略但很管用的参数:

  • replacer
  • space

分别是第二个参数和第三个参数

  1. var a = { a:1, b:2, c:3 };
  2. // replacer支持两种类型参数:数组,方法
  3. // replacer如果是数组,则必须是个字符串数组,传入的数值为要保留下的键值属性
  4. var replacerOne = ["a","b"];
  5. JSON.stringify(a, replacerOne); // "{\"a\":1,\"b\":2}"
  6. // replacer如果是方法,则该方法类似迭代器一样每个属性的键值都会调用一边。要忽略传 undefined
  7. var replacerTwo = function(k,v){ if (k == "a") return undefined; return v; };
  8. JSON.stringify(a, replacerTwo); // "{\"b\":2,\"c\":3}"
  9. // space也支持两种类型参数:数字,字符串
  10. // space如果是数字,则表示每级所要缩减的空格数
  11. var spaceOne = 3;
  12. JSON.stringify(a, null, spaceOne); // "{\n \"a\": 1,\n \"b\": 2,\n \"c\": 3\n}"
  13. // space如果是字符串,则取字符串的前十个字符用于每级缩进
  14. var spaceTwo = "---";
  15. JSON.stringify(a, null, spaceTwo); // "{\n---\"a\": 1,\n---\"b\": 2,\n---\"c\": 3\n}"

这两个参数在开发中好像不经常用,但确实是个非常好用的方法,建议大家记下。

ToNumber

顾名思义就是将非数字转成数字的强制类型转换的抽象方法。
toNumber() 方法其实在JavaScript中并没有,它是在ES规范中定义的。
我们开发中经常使用转换数字的方法有:

  • Number(…)
  • parseInt(…)
  • parseFloat(…)

等等这些方法。通常用于强制类型转换的方法为Number(…)

  1. Number(true); // 1
  2. Number(false); // 0
  3. Number(null); // 0
  4. Number(undefined); // NaN
  5. Number({}); // NaN
  6. Number("42"); // 42
  7. Number("42a"); // NaN 只要字符串不是纯数字就会是NaN,但parseInt/parseFloat的逻辑不同会一直识别到出现不是数字的位数
  8. Number([]); // 0 空数组识别为0
  9. Number(["1"]); // 1 数组如果只有一位时候,就识别第一位数,规则同上。
  10. Number(["1","2"]); // NaN 但length超过1,直接识别为NaN

ToBoolean

顾名思义就是将非布尔值转成布尔值的强制类型转换的抽象方法。
toBoolean() 方法其实在JavaScript中也并没有,它也是在ES规范中定义的。
我们认为原生函数Boolean(…)就是它的实现。
说到布尔值,其实就包含两个值:true,false。但在JavaScript中有一种神奇的存在 —— 假值。

假值

我们可以将JavaScript中的值分成下面两大类:

  • 可以被强制类型转换为false的值
  • 其他(强制类型转换后为true的值)

第一种情况的值就是我们所说的 —— 假值。
在JavaScript规范制定中,列举了布尔强制类型转换中被转换为false的假值:

  • undefined
  • null
  • false
  • +0,-0
  • NaN
  • “”(空字符串)

除了这些值以外,其他的值全都是真值。

三 强制类型转换

接下来就是重点介绍强制类型转换的过程。

显式强制类型转换

说白了就是一看就知道是在做类型转换的强制类型转换,很多开发场景下都有出现。
比如像原生函数Number(…),String(…),Boolean(…)等。

  1. // 数字和字符间转换
  2. var a = 42;
  3. var b = String(a);
  4. var c = "41";
  5. var d = Number(c);
  6. b; // "42"
  7. d; // 41
  8. // 当然也可以使用toString()
  9. var e = a.toString();
  10. e; // "42"
  11. // 转换成数字也可以使用 一元运算符+
  12. +c; // 41

一元运算符 + 通常大家都认为是显示类型转换,虽然平时开发中见的不多。
一元运算符 - 也会进行强制类型转换且在数字前面会加上负号。
由于这两个一元运算符和实际加减运算符一模一样,所以开发中通常不建议一起使用。就算要用在其间需要用空格隔开,否则会被认为使用在是 ++/— 运算符。这里就当作一个小知识吧。
实际中,要获得当前的时间戳也可以使用一元运算符 +

  1. +new Date(); // 1628605543670

又是一个装逼小技巧!

在 ToNumber 抽象中提到的 parseInt方法,其原理和 Number 是不一样的。

  • parseInt:是对字符串参数进行 解析,并获取符合数字标准的值
  • Number:是将其他非数字的值 转换 成数字

解析 和 转换 两个意思还是天差地别的。从上述规则来看,Number在转换过程中,如果参数为字符串,则字符串是不允许出现非数字字符的,否则结果就是 NaN。而parseInt方法是对参数字符串进行解析,也就是运行字符串内含有非数字字符。其逻辑是parseInt会对字符串进行解析数字,直到碰到非数字字符为止并返回结果。(parseFloat也是如此,只是会多识别一个小数点)

  1. parseInt("123"); // 123
  2. parseInt("12a3"); // 12
  3. parseFloat("12.3"); // 12.3
  4. parseFloat("12..3"); // 12 再多个小数点就不行啦

这时候,聪明的小伙伴又要问了:如果参数不是字符串会怎么样?good question!
接下来是一道经典的BUG题

  1. parseInt(1/0, 19); // 结果是18

这个其实不算bug的bug的出现是因为parseInt函数内部处理方式导致的。
当parseInt的参数不是字符串时候,其会对参数先执行toString的方法获取返回值再解析。

  1. parseInt({ toString: function() { return "2" } }); // 2

所以这里 1/0 的结果为 Infinity(在 值 的章节中已经讲过),toString后其字符串值为”Infinity”.
第一个字符”I”实在19进制中表示18,所以保留。第二个字符”n”并不是19进制中合法的数字字符,所以方法暂停取得19进制下数字 I 然后转成10进制,结果为 18.

关于转换成布尔值,其实目前开发中很少使用原生函数Boolean(…) 主流的用法是使用一元运算符 !

  1. var a = 0;
  2. Boolean(0); // false
  3. !!a; // false

其一个!是将值反转(真值变为false,假值变为true),而两个!!就使得值可以转换为对应的布尔值。

隐式强制类型转换

在JavaScript中隐式的强制类型转换是经常被人喷的。因为副作用不明显,新手开发者一般都不知道这里进行了强制类型转换导致出现一些莫名其妙的bug。

1 数字与字符串

最经常见的就是 + 加号运算符的隐式强制类型转换

  1. 42 + "0"; // "420"

当使用 + 加号运算符时候,只要某一边是字符串其就是执行的字符串拼接。记住这个,这个点很重要!
而这里 + 加号运算符在进行强制类型转换时候,调用的对象valueOf()方法。然后将valueOf()方法返回的值用ToString的抽象方法转换为字符串,然后在进行字符串拼接

  1. var a = {
  2. valueOf: function() { return 42 },
  3. toString: function() { return 4 },
  4. }
  5. a + "0"; // "420"
  6. String(a); // "4"

而其他运算符: -,*,/,%,则会将两端的值强制转换为数字就行计算。因为这些运算符也只适用于数字

  1. "42" - "5"; // 37
  2. "42" * "5"; // 210
  3. "42" / "5"; // 8.4
  4. "42" % "5"; // 2

从结果来看,String(a) 和 a + “” 结果一样但其内部实现的过程不同。但实际开发中,a + “”的方式也更为常见一些(到es6后模板字符串是官方所更为推广的转换字符串的方式,但不乏很多JS开发者还在使用+””方式来获取自己需要的字符串)。从这看来隐式强制类型转换也并不是那么糟糕。

2 数字与布尔值

在上述的运算符中: +,-,*,/,%中,在不包含字符串情况下,要是值含有true/false的布尔值,则true转成1、false转成0然后进行计算。

  1. 1 + true; // 2
  2. 1 / false; // Infinity 因为false被转成了0,1/0结果就是Infinity
  3. true % 2; // 1
  4. 5 * false; // 0
  5. true + true; // 2
  6. true + "0"; // "true0" 当然+运算符碰到有字符串的,还是都转成字符串。

3 隐式转换为布尔值

涉及到判断条件时候,其JavaScript都会将判断的值或对象进行隐式强制类型转换为布尔值,如:

  • if(…) 括号内判断式
  • while(…),do{}while(…) 括号内判断式
  • for(,,) 第二个判断式
  • ? : 三元运算符左边判断式
  • ||,&& 左边操作数

其这些地方都会进行隐式的强制类型转换

  1. if(3) {
  2. console.log("true"); // true
  3. }
  4. 3 ? 1 : 0; // 1
  5. 3 && 2; // 2

上面所列举到的位置都会被转成布尔值以便代码来执行判断。

4 逻辑运算符 - ||,&&

||(或)和&&(与)大家应该是经常用的逻辑运算符,也有另一种说法叫:选择器运算符
因为其返回值并不是布尔,而是两个操作数中有且仅有的一个操作数。即根据一定条件选择一个返回。

  1. 3 && 2; // 2
  2. 0 && 2; // 0
  3. 3 || 2; // 3
  4. 0 || 2; // 2

其运算符的判断逻辑如下:

  • ||(或):先对左边操作数(或式)的值进行判断,如果不是布尔值,将其进行ToBoolean的抽象操作转换为布尔值。然后进行判断,为true则返回第一个操作数(或式)的值。如果为false则返回第二个操作数(或式)的值。
  • &&(与):先对左边操作数(或式)的值进行判断,如果不是布尔值,将其进行ToBoolean的抽象操作转换为布尔值。然后进行判断,为true则返回第二个操作数(或式)的值。如果为false则返回第一个操作数(或式)的值。

所以有的开发者将其称为”选择器运算符”还是有道理的。

5 Symbol

Symbol是ES6的新数据类型,用的也不多,稍微记一下。

  1. var a = Symbol("yes");
  2. String(a); // "Symbol(yes)"
  3. a.toString(); // "Symbol(yes)"
  4. a + ""; // TypeError

ES6里Symbol支持显示的强制类型转换,但并不支持 +”” 方式的隐式的强制类型转换。

四 == 和 ===

关于等号,==的官方名称是:宽松对等(loose equals)而===的官方名称是:宽松对等(strict equals)。
日常开发中都是用来做两个值是否相等的”判断”,但其本质上它们是有个很重要的区别,就是在判断条件上。

关于性能

通常情况下大家对 == 和 === 的理解是” ==检查其两边值是否相等,而 === 检查其两边值和类型是否相等。”。哪怕是工作了有几年但没有深入了解过JavaScript语言的开发者也会这样认为。
这句话听上去,===做的事情比 ==做的事情要多,所以有人认为 ==比===性能更好。
但这种说法是存在误区的,正确的说法应该是:==允许在相等比较中进行强制类型转换,而===不允许。
这时候又会有人说那===的性能比==好,这种说法也不能说是错,因为强制类型转换确实会花的时间更多,但可能实际时间也就在微秒级(百万分之一秒)的差别而已。
其==和===都会对两边操作数的类型进行检查,只是它们处理的方式不同罢了。

抽象相等

在ES5规范中11.9.3节”抽象相等比较算法”定义了==运算符的行为。
其中规定如下:

  • 当两边操作数类型相等时候,仅对比它们的值是否相等。
  • 如果两边操作数是对象,则两个对象指向同一个值即为相等,且不发生强制类型转换。

还有一些非常规的比较

  • NaN 不等于 NaN
  • +0 等于 - 0

在这些不涉及强制类型转换上,===的定义是和==的定义一样的

1 数字和字符串

ES5规范定义了:
当 x == y 比较中,其中一方为数字,另一方为字符串,例如 x 为数字, y 为字符串,则对字符串进行ToNumber()的抽象操作后进行比较,即 x == ToNumber(y)。(这里ToNumber()指的是上述中JS的转换数字的方法。)

  1. 42 == "42"; // true

2 布尔与其他

ES5规范定义了:
当 x == y 比较中,其中一方为布尔值时候,例如 x 为布尔值,对布尔值进行ToNumber()的抽象操作后进行比较,即 ToNumber(x) == y。(这里ToNumber()指的是上述中JS的转换数字的方法。)

  1. true == 1; // true
  2. true == "1"; // true

所以个人建议是不要使用==true来进行比对,因为无论如何,其都会将布尔值转换为数字后再进行比较,很耗费性能。(虽然一两个可能影响不大,但如果是大型工程中,这些宽松比较的等式还是很要命的。)而使用===true、===false就没有这个烦恼,因为===压根不进行强制类型转换

3 null 和 undefined

ES5规范定义了:
无论何时何地,null和undefined永远相等,即使 null == undefined 结果为 true.

  1. var a = null;
  2. var b;
  3. a == b; // true
  4. a == 0; // false
  5. a == ""; // false

可以看出null和undefined是彼此的”知音”,它们之间是可以进行互相的强制类型转换。但对于其他假值来说,该不等于还是不等于

4 对象和非对象

ES5规范定义了:
当 x == y 比较中,其中一方为对象(对象、函数、数组)且另一方为基本数据类型(字符串、数字)时候,例如x为对象,y为基本数据类型,则对对象进行ToPrimitive()抽象操作后,进行比较,即 ToPrimitive(x) == y。
TIPS:在这里基本数据类型中并没有提到布尔值是因为布尔值会优先转换为数字后进行比较。
ToPrimitive()抽象操作在上述已经讲过,这里就不多做赘述(数组和对象ToPrimitive的抽象结果和ToString抽象结果一致)。

  1. var a = 42;
  2. var b = [42];
  3. a == b; // true
  4. var c = "42,1";
  5. var d = [42,1];
  6. c == d; // true 这里d就调用了内部ToPrimitive抽象操作即等同于toString()的结果"42,1"
  7. var e = "[object Object]";
  8. var f = {};
  9. e == f; // true 这里f也调用了内部ToPrimitive抽象操作即等同于toString()的"[object Object]"

总结

简单的记法就是:
类型相等只比值,绝不会去转类型。布尔优先转数字,单字符也转数字。null和undefined相等,对象数组转字符。
但还是建议在日常开发中少使用==,或者在有出现布尔、[]、””、0等情况别用。最好还是多使用===来做比较,然后在比对之前自己做显式强制类型转换(String,Number等原生函数),这样代码才会掌握在自己手中!