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转换为0string转换遵守通用规则,行为和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的类型转换十分强大,可以简化代码的书写。但同时也会降低代码可读性,在使用类型转换时要十分小心。也要避免在奇怪的场景中(如图所示)过度使用隐式类型转换,这属于语言的糟粕,不要把它们当做炫技的黑魔法。我们要取其精华去其糟粕。