沿着平滑的曲线学会 JavaScript 中的隐式强制类型转换(实战应用篇)

这一部份内容是承接上一篇的, 建议先阅读沿着平滑的曲线学会 JavaScript 中的隐式强制类型转换(基础篇)数组

前两章讨论了基本数据类型和基本包装类型的关系, 以及两个在类型转换中十分重要的方法: valueOftoString 方法. 接下来的内容创建在前两章之上, 给出判断隐式类型转换结果的方法, 文章最后部分给出了多个练习以及解析, 用以检验文中讨论方法的正确性.bash

3 各类类型之间的强制类型转换

此处谈的强制类型转换指的是除了符号类型(symbol)以外的基本数据类型以及对象之间的类型转换, 对于符号类型(symbol)单独讨论.函数

3.1 ToPrimitive 将变量转换为 基本数据类型

把一个变量转换为 基本数据类型 的转换过程能够被抽象成一种称为 ToPrimitive 的操做, 主意它只是一个抽象出来的名称, 而不是一个具体的方法, 各类数据类型对它的实现才是具体的.post

ToPrimitive 把一个变量转变成一个基本的类型, 会根据变量的类型不一样而采起不一样的操做:ui

  1. 若是这个变量已是基本类型了: 那就不进行转换了, 直接返回这个变量, 就直接用这个变量的值了.spa

  2. 当这个变量是一个对象时: 就调用这个对象的内部的方法 [[DefaultValue]] 来来把对象转换成基本类型。设计

简单来讲, 对于基本数据类型直接返回自己. 对于对象就执行对象自己的 [[DefaultValue]] 方法来得到结果. 那么这个 [[DefaultValue]] 方法是怎么工做的呢, 其实也并不难.code

3.2 [[DefaultValue]] 操做 返回对象的基本数据类型(原始类型)的值

[[DefaultValue]] 方法利用对象内部的 valueOf 方法或 toString 方法返回操做数的基本数据类型(能够指定想获得的类型偏好)。此操做的过程可如此简单理解:对象

在默认的状况下:ip

先调用 valueOf() 方法, 若是返回值是基本类型, 则使用这个值; 不然: 调用 toString() 方法, 获得返回值. 若是这两个方法都没法获得基本数据类型的返回值,则会抛出 TypeError 异常.

另: 对于 Date 对象, 会将这两个方法的调用顺序颠倒过来. 先调用 toString ,若得不到基本类型的值, 就再调用 valueOf. 若都不能获得基本类型的值, 一样抛出 TypeError 异常.

简单总结一下 3.1 部分的内容: 在将一个值转换为基本数据类型的时候, 若是这个值自己就是一个基本数据类型, 则直接使用它本身; 若是这个值是个对象, 就调用对象的两个方法: valueOftoString , 这两个函数获得的结果就是这个对象转换成的基本类型的值.

3.2 基本数据类型之间的类型转换

前一部分讨论了对象如何强制转换为基本数据类型, 夲节主要讨论基本数据类型之间的相互转换. 主要包含三个小部分:

  1. 其余类型数据转换成 字符串
  2. 其余类型转换成 数值
  3. 其余类型转换成 布尔值

下面来一一具体讨论.

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); // ?
复制代码

那么到底应该怎么判断呢? 我的认为能够这样来作, 分红两种简单的状况 :

  1. 若是加号的左右两边都是除字符串以外的基本类型值, 或者是能够经过 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 会首先调用 对象 objvalueOf 方法来获取基本类型的值, 因此获得了 true , 而后输出语句就变成了 console.log(1 + true)console.log(true + true) , 以后 true 被转换成数值类型 1, 式子变成console.log(1+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
复制代码

能够看到输出结果并非布尔值, 而是两个操做数中的一个. 同时还发现, 对于两个相同的操做数, ||&& 操做符的输出状况并不同. 下面来讨论一下缘由.

这两个操做符会根据左边(只判断左边, 不判断右边)的操做数转换成布尔类型以后的值决定返回哪一个操做数, 会先检验左边的操做数的真值, 再作出决定. 具体机制以下:

  1. 对于 || , 当左边的真值为 true 时, 则返回左边的; 不然返回右边的操做数.
  2. 对于 && , 当左边的真值为 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 nullundefined 的比较

nullundefined 在用 == 比较时返回的是 true, 并且除了它们自身以外, 只有这两者相互比较时才返回 true.

换句话说, 除了其自身以外, null 只有和 undefined== 比较时才为 true, 与其余任何值比较时都是 false; 一样的, 除了其自身以外, undefined 只有和 null== 比较时才为 true, 与其余任何值比较时都是 false.

即: 对于 nullundefined 来讲, 只有这三种状况为真:

console.log( null == undefined ); 	// true
console.log( undefined == undefined ); 	// true
console.log( null == null ); 	// true
复制代码

其余特殊状况

  1. NaN 不与任何值等, 即便和自身相比也不相等
console.log(NaN == NaN); 	// false
复制代码
  1. +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
相关文章
相关标签/搜索