谈 JavaScript 中的强制类型转换 (2. 应用篇)
这一部分内容是承接上一篇的, 建议先阅读谈 JavaScript 中的强制类型转换 (1. 基础篇)
前两章讨论了基本数据类型和基本包装类型的关系, 以及两个在类型转换中十分重要的方法: valueOf
和 toString
方法. 接下来的内容建立在前两章之上, 给出判断隐式类型转换结果的方法, 文章最后部分给出了多个练习以及解析, 用以检验文中讨论方法的正确性.
3 各种类型之间的强制类型转换
此处谈的强制类型转换指的是除了符号类型(symbol)之外的基本数据类型以及对象之间的类型转换, 对于符号类型(symbol)单独讨论.
3.1 ToPrimitive
将变量转换为 基本数据类型
把一个变量转换为 基本数据类型 的转换过程可以被抽象成一种称为 ToPrimitive
的操作, 主意它只是一个抽象出来的名称, 而不是一个具体的方法, 各种数据类型对它的实现才是具体的.
ToPrimitive
把一个变量转变成一个基本的类型, 会根据变量的类型不同而采取不同的操作:
-
如果这个变量已经是基本类型了: 那就不进行转换了, 直接返回这个变量, 就直接用这个变量的值了.
-
当这个变量是一个对象时: 就调用这个对象的内部的方法
[[DefaultValue]]
来来把对象转换成基本类型。
简单来说, 对于基本数据类型直接返回本身. 对于对象就执行对象本身的 [[DefaultValue]]
方法来获得结果. 那么这个 [[DefaultValue]]
方法是怎么工作的呢, 其实也并不难.
3.2 [[DefaultValue]]
操作 返回对象的基本数据类型(原始类型)的值
[[DefaultValue]]
方法利用对象内部的 valueOf
方法或 toString
方法返回操作数的基本数据类型(可以指定想得到的类型偏好)。此操作的过程可如此简单理解:
在默认的情况下:
先调用 valueOf()
方法, 如果返回值是基本类型, 则使用这个值; 否则:
调用 toString()
方法, 得到返回值.
如果这两个方法都无法得到基本数据类型的返回值,则会抛出 TypeError
异常.
另: 对于 Date
对象, 会将这两个方法的调用顺序颠倒过来. 先调用 toString
,若得不到基本类型的值, 就再调用 valueOf
. 若都不能得到基本类型的值, 同样抛出 TypeError
异常.
简单总结一下 3.1 部分的内容:
在将一个值转换为基本数据类型的时候, 如果这个值本身就是一个基本数据类型, 则直接使用它自己; 如果这个值是个对象, 就调用对象的两个方法:valueOf
和toString
, 这两个函数得到的结果就是这个对象转换成的基本类型的值.
3.2 基本数据类型之间的类型转换
前一部分讨论了对象如何强制转换为基本数据类型, 夲节主要讨论基本数据类型之间的相互转换. 主要包含三个小部分:
- 其他类型数据转换成 字符串
- 其他类型转换成 数值
- 其他类型转换成 布尔值
下面来一一具体讨论.
3.2.1 其他类型数据转换成 字符串
其他基本数据类型的值转换成字符串类型其实非常简单, 直接变成字符串的形式就可以了. 例如:
null -> "null", undefined -> "undefined", true -> "true", false -> "false", 3.14159 -> "3.14159"
注: 对于非常大或者非常小的数字来说, 转换成字符串会是科学记数法的形式, 例如:
3140000000000000000000 -> "3.14e+21" // 很大的数转换成字符串
0.000000314 -> "3.14e-7" // 很小的数转换成字符串
3.2.2 其他类型数据转换成 数值
其他基本数据类型转换成数值类型也比较简单, 只有字符串需要做非常简单的判断.
具体为:
null -> 0, undefined -> NaN, true -> 1, false -> 0
对于字符串来说, 可细分为一下的情况:
- 空字符串转换为 0
- 若字符串中只含 数字, 加减号, 小数点符号, 则直接转换, 而且忽略前导的 0, 例如:
"3.14" -> 3.14
"-0003.14" -> -3.14 // 前导有 0
-
如果字符串中内容为十六进制, 则转换成的数值为十进制的形式, 例如:
"0xa -> 10"
-
其他情况,则都转换结果为
NaN
, 例如:
"A10" -> NaN
"1A0" -> NaN
"10A" -> NaN
3.2.3 其他类型数据转换成 布尔值
这就更简单了, 只有几个特殊的值转换后为 false
, 除此之外的其他值转换后都为 true
, 这几个特殊的值如下:
NaN, undefined, null, 0, +0, -0, 空字符串""
上述 3.2 部分的内容的记忆是比较简单的, 在处理具体类型转换的问题时只需要灵活运用就可以了, 但是有时候在同一个问题中的同一个变量涉及多个转换过程, 比如从 对象 转为字符串, 然后再从字符串转为 数值.
下一节将会讨论涉及到隐式类型转换的实际应用, 会包含很多例子.
4 涉及到隐式类型转换的情况
JavaScript 中很多常用的操作都会引起隐式的强制类型转换, 下面的部分举几个常见的例子.
4.1 加减乘除号引起的隐式类型转换 + - * /
常用的四则运算操作符在有些时候会引起隐式强制类型的转换
4.1.1 加号 +
在 JavaScript 中, 加号 + 可以用来做加法运算, 也可以用来拼接字符串, 那该怎么判断它执行的是哪个操作呢?
有人说只要加号连接的两个变量其中有一个是字符串时就执行的是拼接操作, 否则就执行加法操作. 这种说法是不完整的, 例如下面这几个例子就无法按照这种说法得到结果, 因为加号两边的变量都不是字符串类型:
// 例 1
console.log(true + true); // ?
console.log(1 + null); // ?
// 例 2
let array = [2, 3];
let a = 1 + array;
let obj = { name: 'doug', age: 4 };
let b = 1 + obj;
console.log(a, b); // ?
那么到底应该怎么判断呢? 个人认为可以这样来做, 分成两种简单的情况 :
- 如果加号的左右两边都是除字符串之外的基本类型值, 或者是可以通过
ToPrimitive
(见 3.1 部分) 抽象操作转换成这些类型的 对象, 那么后台会尝试将这两个变量都转换成数字(具体机制见3.2.2节)进行加法操作.
看下面的实验结果:
console.log( 1 + 1 ); // 2
// true -> 1; false -> 0
console.log( 1 + true ); // 2
console.log( 1 + false ); // 1
console.log( false + 1 ); // 1
console.log( false + true ); // 1
// null -> 0
console.log( 1 + null ); // 1
console.log( true + null ); // 1
// undefined -> NaN
console.log( 1 + undefined ); // NaN
console.log( true + undefined ); // NaN
console.log( null + undefined ); // NaN
// 通过 ToPrimitive 操作返回 number, boolean, null, undefined 基本类型值的对象
// 重写了对象的 valueOf 和 toString 方法
let obj = {
valueOf: function(){
return true;
},
toString: function(){
return 'call obj';
}
}
console.log(1 + obj); // 2
console.log(obj + obj); // 2, 这个例子更加典型
前面的内容提到过在默认情况下 ToPrimitive
会首先调用 对象 obj
的 valueOf
方法来获取基本类型的值, 所以得到了 true
, 然后输出语句就变成了 console.log(1 + true)
和 console.log(true + true)
, 之后 true 被转换成数值类型 1, 式子变成console.log(1+1)
. 实验结果证实了刚才的设想, 适用于 例1 中的情况.
那么 什么情况下进行字符串的拼接操作? 设想如下 :
- 如果加号的左右两边存在 字符串 或者可以通过
ToPrimitive
(见 3.1 部分)抽象操作转换成字符串的对象, 则执行的就是拼接操作.
在 例2 中:
let array = [2, 3];
let a = 1 + array;
let obj = { name: 'doug', age: 4 };
let b = 1 + obj;
console.log(a, b); // ?
变量 a
等于 1 加上一个 数组 array
, 数组是可以通过 ToPrimitive
转化为字符串的, 按照3.1 和 3.2 节的内容, 数组先调用了自己的 valueOf 方法, 发现返回的是 数组本身, 不是基本数据类型; 于是接着调用 toString 方法, 返回了一个各项用 "," 连接的字符串 "2,3"
. 于是现在就有了 let a = 1 + "2,3"
, 是数值和字符串相加, 结果就是 "12,3".
对象 obj
调用 valueOf 返回它本身, 再调用 toString 方法返回字符串 "[object Object]"
. 然后就变成了 let b = 1 + "[object Object]"
, 就变成了数值和字符串相加, 是拼接操作, 所以结果就出来了 "1[object Object]"
.
再看一个例子:
function fn(){ console.log('running fn'); }
console.log(fn + fn);
/*
function fn(){ console.log('running fn'); }function fn(){ console.log('running fn'); }
*/
这个例子的结果用刚才的设想是可以比较容易的得到结果的. 即: 函数 fn
可以通过 ToPrimitive
操作返回一个字符串, 然后式子就变成了 字符串+字符串, 结果就是字符串的拼接.
至此. 上面的论断可能存在不严谨的地方, 欢迎批评指正.
4.1.2 减乘除 - * / 运算符产生的强制类型转换
这三个运算符会将左右两边不是数值类型的变量强制转换成简单数值类型, 然后执行数学运算, 例如:
console.log(true - false); // 1
console.log(true - null); // 1
console.log(true * true); // 1
console.log(2 - undefined); // NaN
console.log([2] - 1); // 1
// [2] -valueOf-> [2] -toString-> "2" -> 2
console.log('3' * 2); //6
console.log('4' / '2'); // 2
let obj = {
toString: function(){ // 重写了 toString 方法, 返回一个字符串
return '4';
}
};
console.log(obj * [2]); // 8
上述几个例子中变量最终都被转换成了数值型的基本数据类型. 其中数组和对象通过 ToPrimitive
(见 3.1 部分)先转换成字符串, 接着强制转换成数值类型再进行数学运算.
4.2 逻辑运算符 ||
和 &&
我们常用将逻辑运算符用在条件判断中, 例如:
if(a || b){
// codes
}
这是很自然的操作.
然而逻辑运算符返回的并不是想象中的布尔类型的 true or false , 而是它左右两个操作数中的一个。例如:
let a = 50;
let b = 100;
console.log(a || b, a && b); // 50 100, 并没有输出 true 或者 false
可以看到输出结果并不是布尔值, 而是两个操作数中的一个. 同时还发现, 对于两个相同的操作数, ||
和 &&
操作符的输出情况并不一样. 下面来讨论一下原因.
这两个操作符会根据左边(只判断左边, 不判断右边)的操作数转换成布尔类型之后的值决定返回哪个操作数, 会先检验左边的操作数的真值, 再做出决定. 具体机制如下:
- 对于
||
, 当左边的真值为 true 时, 则返回左边的; 否则返回右边的操作数. - 对于
&&
, 当左边的真值为 false 时, 直接返回左边的; 否则返回右边的操作数.
可以这样来简单理解:
||
意为 或, 只要两个钟有一个为真就可以了, 所以如果左边为真整体就为真, 直接返回左边就可以了.
&&
意为 且, 要求两边都为真, 如果左边为真, 那么就取决于右边的真假情况了, 所以直接返回右边.
回到开头的例子:
if(a || b){
// codes
}
根据上面的讨论, 可以知道 a || b
并不返回布尔值, 然而 if
却是根据布尔值决定是否执行内部操作的, 那么为什么可以正常执行呢? 原因是 if
语句还要对 a || b
的返回值进行一次隐式强制类型转换, 转换成布尔值, 然后再进行下一步的决定.
类似进行隐式强制类型转换判断的情况还有:
- for循环
- while 和 do while 循环
- 三元运算符 xx? a:b
4.3 非严格相等符号 ==
4.3.1 与严格相等符号 ===
的异同
非严格相等符号(==
)是和严格相等符号(===
)相关的概念. 它们的区别是: ==
允许进行强制类型转换, 再比较转换后的左右操作数; 而 ===
不进行强制类型转换, 直接比较左右两个操作数.
当左右两个操作数的类型相同的时候, 这两种比较符号的效果相同, 运用的原理也相同.
在比较对象的时候, 这两个比较符号的原理也相同: 比较左右两个变量指向的是不是同一个对象.
4.3.2 对象(包括数组和函数)和基本数据类型之间的 ==
比较
在对象与基本数据类型的比较的时候, 对象会通过 ToPrimitive
(见 3.1 部分) 操作返回基本数据类型的值, 然后再进行比较.
4.3.3 布尔值和其他类型的 ==
比较
在布尔值与其他类型比较时, 会先将布尔类型的值转换成数值, 即: true->1, false->0.
4.3.4 字符串和数值的 ==
比较
将字符串转换成数值类型, 然后进行比较
4.3.4 null
和 undefined
的比较
null
和 undefined
在用 ==
比较时返回的是 true, 而且除了它们自身之外, 只有这二者相互比较时才返回 true.
换句话说, 除了其自身之外, null
只有和 undefined
用 ==
比较时才为 true, 与其他任何值比较时都是 false;
同样的, 除了其自身之外, undefined
只有和 null
用 ==
比较时才为 true, 与其他任何值比较时都是 false.
即: 对于 null
和 undefined
来说, 只有这三种情况为真:
console.log( null == undefined ); // true
console.log( undefined == undefined ); // true
console.log( null == null ); // true
其他特殊情况
- NaN 不与任何值等, 即使和自身相比也不相等
console.log(NaN == NaN); // false
- +0 -0 0 三者相等
console.log(0 == +0); // true
console.log(0 == -0); // true
console.log(-0 == +0); // true
总结一下: 对象(包括数组和函数)和其他类型比较时, 要进行类型转换的是对象; 布尔值和其他数据比较时, 要进行类型转换的是布尔值, 转换成数值类型1或0; 在数值和字符串比较时, 要转换类型的是字符串, 转换成数值类型.
5 应用, 举例分析
下面举一些隐式强制类型转换的例子, 用之前讨论的内容判断, 并给出解析:
console.log( "4" == 4 ); // true
// 字符串和数值比较, 字符串转换为数值, 即 "4" -> 4
console.log( "4a" == 4 ); // false
/* 原理同上, 字符串和数值比较, 字符串转换为数值,
但是字符串里包含 "a", 所以转换后是 NaN, 即 "4a" -> NaN;
式子变成 `NaN == 4", 因为 NaN 与任何值都不等, 故为 false */
console.log( "5" == 4 ); // false
// 字符串和数值比较, 字符串转换为数值, 即 "5" -> 4, 式子变成 `5 == 4`, false
console.log( "100" == true ); // false
/* 存在布尔值, 首先布尔值转换为数值, 即: true -> 1,
式子变成: `"100" == 1`, 此时为字符串和数值比较, 字符串转换为数值,
式子变成 `100 == 1`, false
*/
console.log( null == undefined ); // true
console.log( undefined == undefined ); // true
console.log( null == null ); // true
console.log( null == [] ); // false
console.log( null == "" ); // false
console.log( null == {} ); // false
console.log( null == 0 ); // false
console.log( null == false); // false
console.log( undefined == [] ); // false
console.log( undefined == "" ); // false
console.log( undefined == {} ); // false
console.log( undefined == 0 ); // false
console.log( undefined == false ); // false
console.log(null == Object(null) ); // false
console.log(undefined == Object(undefined) ); // false
// 以上的答案比较容易得出, 因为 null 和 undefined 除了自己之外只认识彼此, 文章 4.3.4 部分
console.log( "0" == false ); // true
/* 包含布尔值, 首先布尔值转换为数值, 即: false -> 0,然后式子变成 ` "0" == 0 `;
此时变成了字符串和数值比较, 字符串转换为数值, 即: "0" -> 0,
然后式子变成 ` 0 == 0 `, true */
console.log( 0 == false); // true
// false 转换为 0, true
console.log( false == "" ); // true
/* false -> 0, 式子变成 ` 0 == "" `, 数字和字符串比较;
字符串转换为数值, 即: "" -> 0, 式子变成 ` 0 == 0 `, true */
console.log( false == [] ); // true
// 包含对象和布尔值, 布尔值优先 转换, 随后对象通过 ToPrimitive 操作转换为基本数据类型后比较:
// false -> 0, [] -> "" -> 0; ` 0 == 0 `, true
console.log( false == {} ); // false
// 包含对象和布尔值, 布尔值优先 转换, 随后对象通过 ToPrimitive 操作转换为基本数据类型后比较:
// false -> 0, {} -> "Object Object" -> NaN; ` 0 == NaN ` false
console.log( 0 == ""); // true
// "" -> 0; ` 0 == 0 ` true
console.log( "" == [] ); // true
// 字符串和对象比较, 对象通过 ToPrimitive 操作转换为基本数据类型后比较:
// [] -toString- -> ""; 式子变成 ` "" == "" `, true
console.log( 0 == []); // true
// 数值和对象比较, 对象通过 ToPrimitive 操作转换为基本数据类型后比较:
// [] -toString- -> ""; 式子变成 ` 0 == "" `, 此时是数值类型和字符串比较, 字符串转换为数值
// "" -> 0 ; 式子变成了 ` 0 == 0 `,true
console.log( 0 == {}); // false
// 数值和对象比较, 对象通过 ToPrimitive 操作转换为基本数据类型后比较:
// {} -> "Object Object" -> NaN; ` 0 == NaN ` false
JavaScript 中设计的强制类型转换的内容不止文中提到的这些, 仍存在没有讨论到的内容会在将来讨论.同时文中可能存在错误, 请不吝指正, 谢谢.
参考资料:
- 《JavaScript 高级程序设计》
- 《你不知道的 JavaScript》
- MDN