带你深入浅出的理解隐式转换时js计算中的过程
一、基础概念回顾
什么是隐式转换
JavaScript 是一种弱类型或者说动态语言。这意味着你不用提前声明变量的类型,在程序运行过程中,类型会被自动确定。这也意味着你可以使用同一个变量保存不同类型的数据:
var foo = 42; // foo is a Number now
foo = "bar"; // foo is a String now
foo = true; // foo is a Boolean now
复制代码
- js中,当运算符在运算时,如果两边数据类型不统一,CPU就无法计算,这时我们编译器会自动将运算符两边的数据做一个数据类型转换,转成一样的数据类型再计算。这种无需程序员手动转换,而由编译器自动转换的方式就称为隐式转换。
如:1 + '1' // 执行时不会报错
数据类型
JavaScript 中有7种数据类型,可以分为两类:原始类型、对象类型:
基本数据类型(原始类型):Undefined、 Null、 String、 Number、 Boolean、 Symbol (ECMAScript 2015新增)
复杂数据类型(对象类型):Object
关于 valueOf() 和 toString()
1、valueOf()
默认情况下,valueOf方法由Object后面的每个对象继承。 每个内置的核心对象都会覆盖此方法以返回适当的值。如果对象没有原始值,则valueOf将返回对象本身。
对象 | 返回值 |
---|---|
Array | 返回数组对象本身。 |
Boolean | 布尔值。 |
Date | 存储的时间是从 1970 年 1 月 1 日午夜开始计的毫秒数 UTC。 |
Function | 函数本身。 |
Number | 数字值。 |
Object | 对象本身。这是默认情况。 |
String | 字符串值。 |
Symbol | symbol 原始值。 |
Math 和 Error 对象没有 valueOf 方法。 |
2、toString()
每个对象都有一个toString()方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。默认情况下,toString()方法被每个Object对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回
"[object type]"
,其中type
是对象的类型。
对象 | 返回值 | 是否覆盖自Object的方法 |
---|---|---|
Array | 返回一个字符串,表示指定的数组及其元素。 | 是 |
Boolean | 返回指定的布尔对象的字符串形式。 | 是 |
Date | 返回一个字符串,表示该Date对象。 | 是 |
Function | 返回一个表示当前函数源代码的字符串。 | 是 |
Number | 返回指定 Number 对象的字符串表示形式。 | 是 |
Object | 返回一个表示该对象的字符串。 | - |
String | 返回指定对象的字符串形式。 | 是 |
Symbol | symbol 对象的字符串表示。 | 是 |
Error | 返回一个指定的错误对象(Error object)的字符串表示。 | 是 |
Math | 没有 toString 方法,方法继承自Object。 | 否 |
二、隐式转换规则
隐式转换中主要涉及到三种类型转换:
- 转成string类型:
+ (加法运算符,字符串拼接)
复制代码
- 转成number类型:
++ -- (自增自减运算符)
+ - * / % (算术运算符)
> < >= <= == === != !== (关系、比较运算符)
复制代码
- 转成boolean类型:
&& || ! (逻辑且或非运算符)
复制代码
对象通过ToPrimitive获得原始值
将值转为原始值ToPrimitive(),会执行toNumber或者toString的操作
js引擎内部的抽象操作ToPrimitive有着这样的签名:ToPrimitive(input, PreferredType?)
input是要转换的值,PreferredType是可选参数,可以是Number或String类型。它只是一个转换标志,转化后的结果并不一定是这个参数所值的类型,但是转换结果一定是一个原始值(否则报错)。
- 如果PreferredType被标记为Number,则会进行下面的操作流程来转换输入的值。
1、如果输入的值已经是一个原始值,则直接返回它
2、否则,如果输入的值是一个对象,则调用该对象的valueOf()方法,
如果valueOf()方法的返回值是一个原始值,则返回这个原始值。
3、否则,调用这个对象的toString()方法,如果toString()方法返回的是一个原始值,则返回这个原始值。
4、否则,抛出TypeError异常。
复制代码
- 如果PreferredType被标记为String,则会进行下面的操作流程来转换输入的值。
1、如果输入的值已经是一个原始值,则直接返回它
2、否则,调用这个对象的toString()方法,如果toString()方法返回的是一个原始值,则返回这个原始值。
3、否则,如果输入的值是一个对象,则调用该对象的valueOf()方法,如果valueOf()方法的返回值是一个原始值,则返回这个原始值。
4、否则,抛出TypeError异常。
复制代码
- 既然PreferredType是可选参数,那么如果没有这个参数时,怎么转换呢?PreferredType的值会按照这样的规则来自动设置:
1、该对象为Date类型,则PreferredType被设置为String
2、否则,PreferredType被设置为Number
复制代码
来看下面的表格
Primitive | Number | String | Boolean |
---|---|---|---|
false | 0 | "false" | false |
true | 1 | "true" | true |
0 | 0 | "0" | false |
1 | 1 | "1" | true |
"0" | 0 | "0" | true |
"1" | 1 | "1" | true |
NaN | NaN | "NaN" | false |
Infinity | Infinity | "Infinity" | true |
-Infinity | -Infinity | "-Infinity" | true |
"" | 0 | "" | false |
" " | 0 | " " | true |
"2" | 2 | "2" | true |
"two" | NaN | "two" | true |
[ ] | 0 | "" | true |
[0] | 0 | "0" | true |
[1,2] | NaN | "1,2" | true |
["one"] | NaN | "one" | true |
["one","two"] | NaN | "one,two" | true |
function(){} | NaN | "function(){}" | true |
{ } | NaN | "[object Object]" | true |
null | 0 | "null" | false |
undefined | NaN | "undefined" | false |
三、举几个栗子
1、比较运算符
在 ECMAScript 中,等号由双等号(==)表示,当且仅当两个运算数相等时,它返回 true。
非等号由感叹号加等号(!=)表示,当且仅当两个运算数不相等时,它返回true。
为确定两个运算数是否相等,这两个运算符都会进行类型转换。
执行类型转换的规则如下:
如果一个运算数是 Boolean值,在检查相等性之前,把它转换成数字值。false 转换成 0,true 为 1。
如果一个运算数是字符串,另一个是数字,在检查相等性之前,要尝试把字符串转换成数字。
如果一个运算数是对象,另一个是字符串,在检查相等性之前,要尝试把对象转换成字符串。
如果一个运算数是对象,另一个是数字,在检查相等性之前,要尝试把对象转换成数字。
在比较时,该运算符还遵守下列规则:
值 null 和 undefined 相等。
在检查相等性时,不能把 null 和 undefined 转换成其他值。
如果某个运算数是 NaN,等号将返回 false,非等号将返回 true。
如果两个运算数都是对象,那么比较的是它们的引用值。
如果两个运算数指向同一对象,那么等号返回 true,否则两个运算数不等。
复制代码
[] == 0; true // [] => '' => 0
![] == 0; true // ![] => false => 0
[] == ![]; true // [] => '' => 0 , ![] => false => 0
[] == []; false // 对比的是它们的引用值
{} == !{} false // {} => '[object Object]' => NaN , !{} => false => 0
{} == {} false // 对比的是它们的引用值
复制代码
const a = {
i: 1,
valueOf: function () {
return a.i++;
},
toString: function () {
return a.i++;
}
}
if (a == 1 && a == 2 && a == 3) {
console.log('hello world!');
}
//a先会调用valueOf()方法,如有原始值则返回这个原始值
//否则,调用这个对象的toString()方法
//所以改写其中任一方法均可
复制代码
这么坑的面试题,你咋不上天呢?
特殊情况(无视规则):如果数据是undefined 、 null 、 NaN , 得出固定结果
在检查相等性时,不能把 null 和 undefined 转换成其他值。(重要)
undefined == undefined // true
undefined === undefined // true
undefined == null // true
null == null // true
null === null // true
null == 0 // false null没有转换
NaN == NaN // false
NaN === NaN // false
NaN == null // false
NaN == undefined // false
复制代码
2、关系运算符
"2" > 10; // false 2 < 10
"2" > "10"; // true 比较的是Unicode
"abc" > "b"; // false 比较的是Unicode
"abc" > "abd" // true 比较的是Unicode,第一个相等且还有后续就对比第二个,得出大小的结果就终止对比了
复制代码
3、字符串连接符与算术运算符
1 + "true"; // "1true"
1 + true; // 2
1 + undefined; // NaN
1 + null; // 1
复制代码
等等,是不是忘了什么?
四、 symbol
在 JavaScript 中,虽然大多数类型的对象在某些操作下都会自动的隐式调用自身的 valueOf() 方法或者 toString() 方法来将自己转换成一个原始值,但 symbol 对象不会这么干,symbol 对象无法隐式转换成对应的原始值:
Object(Symbol("foo")) + "bar";
// TypeError: can't convert symbol object to primitive
// 无法隐式的调用 valueOf() 方法
Object(Symbol("foo")).valueOf() + "bar";
// TypeError: can't convert symbol to string
// 手动调用 valueOf() 方法,虽然转换成了原始值,但 symbol 原始值不能转换为字符串
Symbol("foo").toString() + "bar"
// "Symbol(foo)bar",就相当于下面的:
Object(Symbol("foo")).toString() + "bar";
// "Symbol(foo)bar",需要手动调用 toString() 方法才行
复制代码
换句话说,在 Symbol.toPrimitive() 方法内部判断了值类型,根据类型进行后续不同的操作,而不是简单的调用 toString() & valueOf()方法,对于 Symbol 类型,它的处理就是抛出异常。
资料查阅:MDN 、 w3school