js中的值可以从一种类型转换为另一种类型,这种行为被称为强制类型转换。
抽象操作规则
所有的规则以es6为准,和其他文章的es5版本规则稍有不同,详情请查看es6语言规范
主要介绍强制类型转换最常用的4种抽象操作:转换为基本类型ToPrimitive
,转换为字符串ToString
,转换为数字ToNumber
,转换为布尔值ToBoolean
。
这一节主要抽象操作的规则,不会讲何时会触发这些操作操作。结合后续实例一起阅读效果更佳。
ToPrimitive
抽象操作ToPrimitive
负责处理将原始类型转换为基本类型。
可以将ToPrimitive
理解为一个形如ToPrimitive(input[, preferredType])
的函数。其中input
表示输入值;preferredType
表示进行抽象操作时所倾向的转化类型,是一个可选值,它的值是"string"
,"number"
或者"default"
之一。
ToPrimitive
转换规则如下:
- 当输入类型是基本类型(
undefined
,null
,boolean
,number
,string
,symbol
)时,直接返回输入值 - 当输入类型是对象类型(
object
)时,则:- 判断对象是否具有Symbol.toPrimitive方法
- 如果有:如果该方法返回值是基本类型,就返回该值作为结果;如果该方法返回值是对象类型,报
TypeError
错误
- 如果有:如果该方法返回值是基本类型,就返回该值作为结果;如果该方法返回值是对象类型,报
- 如果
preferredType
被指定为"string"
- 判断该对象是否具有
toString
方法,如果有并且该方法返回基本类型值,返回该值作为结果 - 判断该对象是否具有
valueOf
方法,如果有并且该方法返回基本类型值,返回该值作为结果 - 报
TypeError
错误
- 判断该对象是否具有
- 如果
preferredType
被指定为"number"
- 判断该对象是否具有
valueOf
方法,如果有并且该方法返回基本类型值,返回该值作为结果 - 判断该对象是否具有
toString
方法,如果有并且该方法返回基本类型值,返回该值作为结果 - 报
TypeError
错误
- 判断该对象是否具有
- 如果
preferredType
未被指定类型(即为"default"
)- 如果操作的对象是
Date
,则将preferredType
重新指定为"string"
(因为Date转换为字符串的场景使用的比较多),并按以上规则转换 - 否则指定为
"number"
,并按以上ToPrimitive
规则转换
- 如果操作的对象是
- 判断对象是否具有Symbol.toPrimitive方法
进行ToPrimitive
操作的对象,如果不具有Symbol.toPrimitive
方法,会依赖toString
和valueOf
两个方法。某些对象本身就实现这些方法或者其中之一。即使没有实现,Object.prototype
中就包含了toString
和valueOf
这两个方法,所以一般的对象中其实都存在这两个方法(具体原因参见原型链)。不同对象调用这些方法的行为会有差别,请注意区分。
而使用Object.create(null)
生成的对象是一个真空对象,什么属性方法都没有。所以对这种对象直接进行ToPrimitive
操作(不在生成对象后手动添加相关方法)会报TypeError
错误。
ToString
抽象操作ToString
负责处理将原始类型转换为基本类型string
。
可以将ToString
理解为一个形如ToString(input)
的函数。
ToString
转换规则如下:
string
类型不转换直接返回symbol
会报TypeError
错误null
转换为"null"
,undefined
转换为"undefined"
;true
转换为"true"
,false
转换为""false"
number
转换遵守通用规则,行为和new String(numberValue)
一致,其中:- 特殊值
NaN
返回"NaN"
,Infinity
返回"Infinity"
,-Infinity
返回"-Infinity"
- 零值
0
或者-0
都返回"0"
- 对于极大或者极小值使用指数形式的字符串
- 特殊值
object
会先将preferredType
指定为"string"
进行ToPrimitive
抽象操作;再对得到的基本类型按照以上ToString
规则转换
ToNumber
抽象操作ToNumber
负责处理将原始类型转换为基本类型number
。
可以将ToNumber
理解为一个形如ToNumber(input)
的函数。
ToNumber
转换规则如下:
number
类型不转换直接返回symbol
会报TypeError
错误null
转换为0
,undefined
转换为NaN
,true
转换为1
,false
转换为0
string
转换遵守通用规则,行为和new Number(stringValue)
一致,其中:- 空字符串转换为
0
- 合法的二进制,八进制,十六进制的字符串数字都会被转换为十进制数字
- 转换数字失败时返回
NaN
- 空字符串转换为
object
会先将preferredType
指定为"number"
进行ToPrimitive
抽象操作;再对得到的基本类型按照以上ToNumber
规则转换
ToBoolean
抽象操作ToBoolean
负责处理将原始类型转换为基本类型boolean
。
可以将ToBoolean
理解为一个形如ToBoolean(input)
的函数。
ToBoolean
转换规则如下:
- 假值(falsy value),包括
undefined
,null
,false
,-0
,0
,NaN
,""
(空字符串)都会被强制转换为false
- 真值(truthy value),即除假值之外的一切值都会被强制转换为
true
以上四种抽象操作流程图:
显式转换
显式转换很容易识别,我们应该尽可能使用它,保证在编码时将类型转换表达清楚,提高代码可读性。
const a = new String(10); // 显式ToString转换,number转string
const b = new String(true); // 显式ToString转换,boolean转string
const c = new Number("10"); // 显式ToNumber转换,string转number
const d = new Number(true); // 显式ToNumber转换,boolean转number
const e = new Boolean("10"); // 显式ToBoolean转换,string转boolean
const f = new Boolean(0); // 显式ToBoolean转换,number转boolean
形如上述结构都是显式类型转换,会将传入的参数转换为内置构造函数对应的基础类型,并将转换后的结果存储在内部的[[PrimitiveValue]]
中。
注意,显式调用toString
方法和ToString
抽象操作是不一样的:
const obj = {
toString() {
return {};
},
};
console.log(obj.toString()); // {}
console.log(new String(obj)); // TypeError
调用toString
方法时,并不一定需要返回字符串,我们可以自定义返回值。而对于ToString
抽象操作来说,obj
调用toString
如果返回的不是基本类型会直接报错(因为当前obj
没有指定Symbol.toPrimitive
,且默认的valueOf
方法返回的是该对象本身,同样不是基本类型)。
隐式转换
隐式转换则没有那么容易识别,主要出现在运算符操作中。
+
运算符
一元形式
一元形式的+
运算符(即只有一个操作数)会触发ToNumber
的抽象操作。
const a = "12.5";
const b = true;
const obj = { a: 1 };
console.log(+a); // 12.5
console.log(+b); // 1
console.log(+obj); // NaN
obj.valueOf = function () {
return "4e5";
};
console.log(+obj); // 400000
按照ToNumber
转换规则。 a
,b
很容易理解。我们重点说说obj
:
-
由于
obj
是对象类型,所以首先进行ToPrimitive(obj, "number")
obj
自身不在Symbol.toPrimitive
;继续查找valueOf
并在原型链中找到,它返回结果是它本身,不是基本类型;继续查找toString
并在原型链中找到,它返回结果是字符串"[object Object]"
,返回该值
-
对
"[object Object]"
继续做ToNumber
转换,得到NaN
而在我们手动为obj
加上valueOf
方法后:
- 由于
obj
是对象类型,所以首先进行ToPrimitive(obj, "number")
obj
自身不在Symbol.toPrimitive
;继续查找valueOf
并在对象自身中找到,它返回结果是字符串"4e5"
,返回该值- 对
"4e5"
继续做ToNumber
转换,得到400000
二元形式
二元形式的+
运算符,转换规则更复杂:
- 对
+
两边的操作数分别进行ToPrimitive
转换 - 转换后的值,如果存在
string
类型的值,则对+
两边转换后的值分别进行ToString
转换后进行拼接操作 - 转换后的值如果是其他类型,则对
+
两边转换后的值分别进行ToNumber
转换后进行相加
const a = 1 + "str" + false;
console.log(a); // "1strfalse"
const obj1 = {
valueOf() {
return 1;
},
};
const obj2 = {
toString() {
return "a";
},
};
console.log(1 + obj1); // 2
console.log("1" + obj2); // "1a"
console.log(obj1 + obj2); // "1a"
我们逐个分析:
1 + "str" + false
- 都是
+
运算符,所以从左到右依次计算 - 首先是
1 + "str"
,两个都是基本类型,所以ToPritimive
转换结果不变 - 因为
"str"
是字符串,所以都进行ToString
转换,"str"
结果不变,1
变成"1"
- 进行拼接操作结果为
"1str"
- 继续和
false
相加,结果与上面步骤类似,所以得到最终结果"1strfalse"
- 都是
1 + obj1
- 首先对两边的操作数分别做
ToPrimitive
(不指定preferredType
)转换,1
不变,obj1
转换为1
- 结果为
1 + 1
,由于不存在string
类型值,所以两边分别进行ToNumber
转换,由于都是数字类型,所以结果不变,最后相加结果2
- 首先对两边的操作数分别做
"1" + obj2
- 首先对两边的操作数分别做
ToPrimitive
转换,"1"
不变,obj1
转换为"a"
- 进行拼接操作结果为
"1a"
- 首先对两边的操作数分别做
"obj1 + obj2"
转换过程类似,不再赘述
-
,*
,/
运算符
这三个运算符只适用于数字,所以转换规则比起+
更简单,不会有ToString
。和一元+
规则类似,对所有的操作数进行ToNumber
转换。
const a = "12.5";
const obj = {
a: 1,
valueOf() {
return this.a;
},
};
console.log(-a); // -12.5
console.log(-obj); // -2
console.log(a - obj); // 10.2
console.log(a * obj); // 25
console.log(a / obj); // 6.25
==
和===
运算符
宽松相等(==
)和严格相等===
都常用于判断两个值是否相等。我们通常认为===
不仅比较“值”是否相等,还会比较”类型“是否相等,而==
只比较“值”是否相等。所以有以下结果:
console.log("1"==1); // true
console.log("1"===1); // // false
实际上这并不准确,正确的解释是:==
在比较时会进行隐式类型转换,而===
不会。实际上==
做的事更多。
==
运算符的行为由“抽象相等比较算法”定义,主要规则:
- 如果两个操作数类型相同,则按相同类型比较方法进行比较
- 如果是
number
类型,判断两者值是否相同,注意特殊值的比较 - 如果是
string
类型,判断两者是否是完全相同的字符序列 - 如果是
object
类型,判断两者是使用引用同一个对象
- 如果是
- 如果两个操作数类型不同
- 对两边的操作数分别进行
ToPrimitive
转换;如果转换后的值类型相同,按相同类型判断规则比较 - 如果转换后的基本值一个是
undefined
类型,另一个是null
类型,判断为true
- 否则,继续对两边转换后的值分别进行
ToNumber
转换后再按相同类型判断规则比较
- 对两边的操作数分别进行
在两个操作数类型相同时,===
和==
工作原理相同。如果两个操作数类型不同,===
将直接返回false
。
console.log("true" == 42); // false
console.log(3 == true); // false
const obj = [1, 2, 3];
console.log(obj == "1,2,3"); // true
我们逐个分析:
"true" == 42
- 类型不同,但都是基本类型,所以
ToPrimitive
结果不变 - 都不是
undefined
或者null
之一,继续进行ToNumber
,得到结果NaN
和42
,返回false
- 类型不同,但都是基本类型,所以
3 == true
- 类型不同,但都是基本类型,所以
ToPrimitive
结果不变 - 都不是
undefined
或者null
之一,继续进行ToNumber
,得到结果3
和1
,返回false
- 类型不同,但都是基本类型,所以
obj == "1,2,3"
- 类型不同,首先进行
ToPrimitive
,得到结果"1,2,3"
和"1,2,3"
- 转换后类型相同,按照字符串比较方式比较,返回
true
- 类型不同,首先进行
github大佬dorey制作了一份图表,列出了各种相等比较的情况:
注意:switch
中case
所使用的比较是===
>
,<
,>=
,<=
运算符
比较运算符的行为由“抽象比较算法”定义,并且没有类似于===
这样的严格比较。所以它们都可能会发生隐式转换,主要规则:
- 先将
preferredType
指定为"number"
,对两个操作数分别进行ToPrimitive
转换 - 如果转换后的值都是
string
类型,则按字符串比较规则判断 - 否则,对转换后的值继续进行
ToNumber
转换,再对转换后的值按照数字的比较规则判断
const a = [42];
const b = ["043"];
console.log(a < b); // false
console.log(10 < a); // true
const obj1 = { a: 1 };
const obj2 = { a: 2 };
console.log(obj1 < obj2); // false
我们逐个分析:
-
a < b
- 首先对两边的操作数分别进行
ToPrimitive(value,"number")
转换,得到"42"
和"043"
- 转换后的值都是
string
类型,按照字符串比较规则"42" > "043"
,返回false
- 首先对两边的操作数分别进行
-
10 < a
- 首先对两边的操作数分别进行
ToPrimitive(value,"number")
转换,得到10
和"42"
- 转换后的值不都是
string
类型,继续进行ToNumber
转换,得到10
和42
- 按照数字比较规则
10 < 42
,返回true
- 首先对两边的操作数分别进行
-
obj1 < obj2
- 首先对两边的操作数分别进行
ToPrimitive(value,"number")
转换,得到"[object Object]"
和"[object Object]"
- 转换后的值都是
string
类型,按照字符串比较规则"[object Object]" === "[object Object]"
,返回false
- 首先对两边的操作数分别进行
实际上,“抽象比较算法”只会比较<
和>
的情况;>=
和<=
会被转换。如a >= b
会被转换为!(a < b)
。所以>=
并不是>
和==
结果的组合。==
采用了完全不一样的比较算法。
const a = { b: 42 };
const b = { b: 43 };
console.log(a < b); // false
console.log(a > b); // false
console.log(a == b); // false
console.log(a <= b); // true
console.log(a >= b); // true
首先,不管是a < b
还是a >b
,对两边分别进行ToPrimitive(value,"number")
转换,得到"[object Object]"
和"[object Object]"
,可以看到两个字符串相等,所以a < b
还是a >b
都为false
。而a <= b
会被转换会!(a>b)
,显然结果是true
。同理a >= b
也是true
。
而a == b
是采用“抽象相等比较算法”,两者都是对象,类型相同,对于对象就要判断两者是使用引用同一个对象,显然不是,所以结果为false
。
隐式转换为布尔相关运算符及运算符
下面的情况会发生布尔值隐式强类型转换:
if (..)
语句中的条件判断表达式for ( .. ; .. ; .. )
语句中的条件判断表达式(第二个)while (..)
和do..while(..)
循环中的条件判断表达式- 三元
? :
中的条件判断表达式 - 逻辑运算符
||
和&&
左边的操作数 !
和!!
这些表达式和运算符都会让操作数进行ToBoolean
转换,再用于判断。
const a = 10;
console.log(!a); // false
console.log(!!a); // true
!
会先让操作数进行ToBoolean
操作,然后再取反;而!!
相当于两次取反,得到原来的转换结果,所以!!
常用于将其他类型的值转换为布尔值。
if
等表达式也会先对其中的操作数进行ToBoolean
转换后再判断true
或者false
:
const a = {};
if (a) {
// 会进入内部执行
}
const b = a ? true : false; // true
&&
和||
虽然常被看做逻辑运算符,但其实它们更接近”短路运算符“,用于返回两个操作数中的一个,而不一定返回布尔值。
const a = 10 || "abc"; // 10
const b = 10 && "abc"; // "abc"
const c = undefined || "abc"; // "abc"
const d = undefined && "abc"; // undefined
&&
和||
会首先对第一个操作数执行条件判断,即执行ToBoolean
转换再判断true
或者false
:
- 对
&&
来说:如果判断结果是true
则返回第二个操作数,否则返回第一个操作数。a && b
相当于a ? b : a
- 对
||
来说:如果判断结果是true
则返回第一个操作数,否则返回第二个操作数。和&&
刚好相反。a || b
相当于a ? a : b
当if
结合&&
和||
使用时:
// 首先(10 && "abc")返回"abc", if再对"abc"进行ToBoolean操作得到true
if (10 && "abc") {
// 会进入内部执行
}
// 首先(0 || "")返回"", if再对""进行ToBoolean操作得到false
if (0 || "") {
// 不会进入内部执行
}
注意
js的类型转换十分强大,可以简化代码的书写。但同时也会降低代码可读性,在使用类型转换时要十分小心。也要避免在奇怪的场景中(如图所示)过度使用隐式类型转换,这属于语言的糟粕,不要把它们当做炫技的黑魔法。我们要取其精华去其糟粕。