这里有一份简洁的前端知识体系等待你查收,看看吧,会有惊喜哦~如果觉得不错,恳求star哈~
类型转换
讲完了基本类型,我们来介绍一个现象:类型转换。
== 运算
因为JS是弱类型语言,所以类型转换发生非常频繁,大部分我们熟悉的运算都会先进行类型转换。大部分类型转换符合人类的直觉,“ == ”运算除外。因为试图实现跨类型的比较,“ == ”运算的规则复杂到几乎没人可以记住。
虽然==的行为很复杂,但是归根结底,类型不同的变量比较时 == 运算只有三条规则:
- undefined与null相等;
- 字符串和bool都转为数字再比较;
- 对象转换成基础类型再比较。
这样我们就可以理解一些不太符合直觉的例子了,比如:
false == '0' // true
true == 'true' // false
[] == 0 // true
[] == false // true
new Boolean('false') == false // false
这里不太符合直觉的有两点:
- 即使字符串与boolean比较,也都要转换成数字;
- 对象如果转换成了基础类型跟等号另一边类型恰好相同,则不需要转换成数字。
此外,== 的行为也经常跟if的行为(转换为boolean)混淆。总之,仅在确认 == 发生在Number和String类型之间时使用。
如果你觉得麻烦的话,这里提供了流程图:
下面看一个典型的例子:为什么[] == ![]?分析如下:
[] == ![]
↓
[] == false
↓
"" == false
↓
0 == false
↓
0 == 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种,分别是:
- 转换为布尔值
- 转换为数字
- 转换为字符串
- 转换为对象
其中,“转换为数字”跟“转换为字符串”主要发生在字符串跟数字之间。我们逐一分析。
转换为布尔值
在条件判断时,除了 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 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"
如果你对于答案有疑问的话,请看解析:
- 对于第一行代码来说,触发特点2,所以将数字 1 转换为字符串,得到结果 '11'
- 对于第二行代码来说,触发特点1,所以将 true 转为数字 1
- 对于第三行代码来说,触发特点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
比较运算符
- 如果是对象,先进行拆箱操作。
- 如果是字符串,就通过 unicode 字符索引来比较
let a = {
valueOf() {
return 0
},
toString() {
return '1'
}
}
a > -1 // true
在以上代码中,因为 a 是对象,所以会通过 valueOf 转换为原始类型再比较值。