该节会全面介绍强制类型转换的优缺点
1、值类型转换
将值从一种类型转换为另一种类型通常称为类型转换,这是显式的情况;隐式的情况称为强制类型转换
JS中的强制类型转换总是返回标量基本类型值(参见第 2 章),如字符串、数字和布尔值,不会返回对象和函数。
也可以这样来区分:类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在强制类型转换动态类型语言的运行时。
然而在 JS中通常将它们统称为强制类型转换,用“隐式强制类型转换”(implicit coercion)和“显式强制类型转换”(explicit coercion)来区分。
var a = 42;
var b = a + ""; // 隐式强制类型转换
var c = String( a ); // 显式强制类型转换
对变量 b 而言,强制类型转换是隐式的;由于 + 运算符的其中一个操作数是字符串,所以是字符串拼接操作,结果是数字 42 被强制类型转换为相应的字符串 “42”。而 String(…) 则是将 a 显式强制类型转换为字符串。
2、抽象值操作
在ES5中,定义了一些"抽象操作"(仅供内部使用的操作)和转换规则。着重介绍ToString、ToNumber 和 ToBoolean和ToPrimitive
2.1、ToString
该操作负责处理非字符串到字符串的强制类型转换
基本类型值的字符串化规则为:null 转换为 “null”,undefined 转换为 “undefined”,true转换为 “true”。数字的字符串化则遵循通用规则
对普通对象来说,除非自行定义,否则 toString()(Object.prototype.toString())返回内部属性 [[Class]] 的值
数组的默认 toString() 方法经过了重新定义,将所有单元字符串化以后再用 “,” 连接起来:
var a = [1,2,3];
a.toString(); // "1,2,3"
toString() 可以被显式调用,或者在需要字符串化时自动调用。
JOSN字符串化
工具函数 JSON.stringify(…) 在将 JSON 对象序列化为字符串时也用到了 ToString。
对大多数简单值来说,JSON 字符串化和 toString() 的效果基本相同,只不过序列化的结果总是字符串
JSON.stringify( 42 ); // "42"
JSON.stringify( "42" ); // ""42"" (含有双引号的字符串)
JSON.stringify( null ); // "null"
JSON.stringify( true ); // "true"
JSON.stringify(…) 在对象中遇到 undefined、function 和 symbol 时会自动将其忽略,在数组中则会返回 null(以保证单元位置不变)。
JSON.stringify( undefined ); // undefined
JSON.stringify( function(){} ); // undefined
JSON.stringify(
[1,undefined,function(){},4]
); // "[1,null,null,4]"
JSON.stringify(
{ a:2, b:function(){} }
); // "{"a":2}"
对包含循环引用的对象执行 JSON.stringify(…) 会出错
如果要对含有非法 JSON 值的对象做字符串化,或者对象中的某些值无法被序列化时,就需要定义 toJSON() 方法来返回一个安全的 JSON 值
var o = { };
var a = {
b: 42,
c: o,
d: function(){}
};
// 在a中创建一个循环引用
o.e = a;
// 循环引用在这里会产生错误
// JSON.stringify( a );
// 自定义的JSON序列化
a.toJSON = function() {
// 序列化仅包含b
return { b: this.b };
};
JSON.stringify( a ); // "{"b":42}"
也就是说,toJSON() 应该“返回一个能够被字符串化的安全的 JSON 值”,而不是“返回一个 JSON 字符串”。
我们可以向 JSON.stringify(…) 传递一个可选参数 replacer,它可以是数组或者函数,用来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除
var a = {
b: 42,
c: "42",
d: [1,2,3]
};
JSON.stringify( a, ["b","c"] ); // "{"b":42,"c":"42"}"
JSON.string 还有一个可选参数 space,可以是一个数字或者一个字符串。用来指定输出的缩进格式
const obj = { name: 'John', age: 30 };
const jsonStr = JSON.stringify(obj, null, 4);
console.log(jsonStr);
{
"name": "John",
"age": 30
}
2.2、ToNumber
有时我们需要将非数字值当作数字来使用,比如数学运算。其中 true 转换为 1,false 转换为 0。undefined 转换为 NaN,null 转换为 0。
不同之处是 ToNumber 对以 0 开头的十六进制数并不按十六进制处理。
对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。为了将值转换为相应的基本类型值,抽象操作 ToPrimitive会首先(通过内部操作 DefaultValue,)检查该值是否有 valueOf() 方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString()的返回值(如果存在)来进行强制类型转换。如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误
使用 Object.create(null) 创建的对象 [[Prototype]] 属性为 null,并且没有 valueOf() 和 toString() 方法,因此无法进行强制类型转换
2.3、ToBoolean
我们可以将 1 强制类型转换为 true,将 0 强制类型转换为 false,反之亦然,但它们并不是一回事。
假值
JS中的值可以分为以下两类:
(1) 可以被强制类型转换为 false 的值
(2) 其他(被强制类型转换为 true 的值)
以下列举了布尔强制类型转换所有可能出现的结果:
以下这些是假值:undefined、null、false、+0、-0 和 NaN、“”,假值的布尔强制类型转换结果为 false,我们可以理解为假值列表以
外的值都是真值,规定所有的对象都是真值
假值对象
var a = new Boolean( false );
var b = new Number( 0 );
var c = new String( "" );
var d = Boolean( a && b && c );
d; // true
它们都是封装了假值的对象。那它们究竟是 true 还是 false 呢?d 为 true,说明 a、b、c 都为 true。
假值对象看起来和普通对象并无二致(都有属性,等等),但将它们强制类型转换为布尔值时结果为 false。
最常见的例子是 document.all,它是一个类数组对象,包含了页面上的所有元素,由DOM(而不是 JavaScript 引擎)提供给 JavaScript 程序使用。它以前曾是一个真正意义上的对象,布尔强制类型转换结果为 true,不过现在它是一个假值对象。
真值
真值就是假值列表之外的值。
var a = "false";
var b = "0";
var c = "''";
var d = Boolean( a && b && c );
d; // true
也就是说真值列表可以无限长,无法一一列举,所以我们只能用假值列表作为参考。
3、显式强制类型转换
显式强制类型转换是那些显而易见的类型转换,很多类型转换都属于此列
3.1、字符串与数字
字符串和数字之间的转换是通过 String(…) 和 Number(…) 这两个内建函数(原生构造函数)来实现的
String(…) 遵循前面讲过的 ToString 规则,将值转换为字符串基本类型。Number(…) 遵循前面讲过的 ToNumber 规则,将值转换为数字基本类型。一目了然,所以我们将它们归为显式强制类型转换。
除了 String(…) 和 Number(…) 以外,还有其他方法可以实现字符串和数字之间的显式转换:
var a = 42;
var b = a.toString();
var c = "3.14";
var d = +c;
b; // "42"
d; // 3.14
a.toString() 是显式的(“toString”意为“to a string”),不过其中涉及隐式转换。因为toString() 对 42 这样的基本类型值不适用,所以 JavaScript 引擎会自动为 42 创建一个封装对象,然后对该对象调用 toString()。这里显式转换中含有隐式转换。
在JS开源社区中,一元运算 + 被普遍认为是显式强制类型转换。
日期显式转换为数字
建议应该使用 Date.now() 来获得当前的时间戳,使用 new Date(…).getTime() 来获得指定时间的时间戳
console.log(Date.now()) // 1701266286450
~ 运算符
~即字位操作非,虽然严格说来并非强制类型转换(因为返回值类型并没有发生变化),但字位运算符(如 |和 ~)和某些特殊数字一起使用时会产生类似强制类型转换的效果,返回另外一个数字
~。它首先将值强制类型转换为 32 位数字,然后执行字位操作“非”(对每一个字位进行反转)。
在 -(x+1) 中唯一能够得到 0(或者严格说是 -0)的 x 值是 -1。也就是说如果 x 为 -1 时,~和一些数字值在一起会返回假值 0,其他情况则返回真值。
然而这与我们讨论的内容有什么关系呢?
-1 是一个“哨位值”,哨位值是那些在各个类型中(这里是数字)被赋予了特殊含义的值。在 C 语言中我们用 -1 来代表函数执行失败,用大于等于 0 的值来代表函数执行成功。
JS中字符串的 indexOf(…) 方法也遵循这一惯例,该方法在字符串中搜索指定的子字符串,如果找到就返回子字符串所在的位置(从 0 开始),否则返回 -1,indexOf(…) 不仅能够得到子字符串的位置,还可以用来检查字符串中是否包含指定的子字符串,相当于一个条件判断。例如
var a = "Hello World";
if (a.indexOf( "lo" ) >= 0) { // true
// 找到匹配!
}
if (a.indexOf( "lo" ) != -1) { // true
// 找到匹配!
}
if (a.indexOf( "ol" ) < 0) { // true
// 没有找到匹配!
}
if (a.indexOf( "ol" ) == -1) { // true
// 没有找到匹配!
}
>= 0 和 == -1 这样的写法不是很好,称为“抽象渗漏”,意思是在代码中暴露了底层的实现细节,这里是指用 -1 作为失败时的返回值,这些细节应该被屏蔽掉。~ 和 indexOf() 一起可以将结果强制类型转换(实际上仅仅是转换)为真 / 假值:
var a = "Hello World";
~a.indexOf( "lo" ); // -4 <-- 真值!
if (~a.indexOf( "lo" )) { // true
// 找到匹配!
}
~a.indexOf( "ol" ); // 0 <-- 假值!
!~a.indexOf( "ol" ); // true
if (!~a.indexOf( "ol" )) { // true
// 没有找到匹配!
}
如果 indexOf(…) 返回 -1,~ 将其转换为假值 0,其他情况一律转换为真值。
字位截除
一些开发人员使用 ~~ 来截除数字值的小数部分,以为这和 Math.floor(…) 的效果一样,实际上并非如此。
~~ 中的第一个 ~ 执行 ToInt32 并反转字位,然后第二个 ~ 再进行一次字位反转,即将所有字位反转回原值,最后得到的仍然是 ToInt32 的结果,~~ 和 !! 很相似
对 ~~ 我们要多加注意。首先它只适用于 32 位数字,更重要的是它对负数的处理与 Math.floor(…) 不同。
Math.floor( -49.6 ); // -50
~~-49.6; // -49
3.2、显式解析数字字符串
解析字符串中的数字和将字符串强制类型转换为数字的返回结果都是数字。但解析和转换两者之间还是有明显的差别。
var a = "42";
var b = "42px";
Number( a ); // 42
parseInt( a ); // 42
Number( b ); // NaN
parseInt( b ); // 42
解析允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停止。而转换不允许出现非数字字符,否则会失败并返回 NaN。解析字符串中的浮点数可以使用 parseFloat(…) 函数。
parseInt(…) 针对的是字符串值。向 parseInt(…) 传递数字和其他类型的参数是没有用的,比如 true、function(){…} 和 [1,2,3]。
parseInt(…) 会根据字符串的第一个字符来自行决定基数。如果第一个字符是 x 或 X,则转换为十六进制数字。如果是 0,则转换为八进制数字。默认为十进制
解析非字符串
3.3、显式转换为布尔值
来看看非布尔值转换为布尔值的情况
与前面的 String(…) 和 Number(…) 一样,Boolean(…)(不带 new)是显式的 ToBoolean 强制类型转换
var a = "0";
var b = [];
var c = {};
var d = "";
var e = 0;
var f = null;
var g;
Boolean( a ); // true
Boolean( b ); // true
Boolean( c ); // true
Boolean( d ); // false
Boolean( e ); // false
Boolean( f ); // false
Boolean( g ); // false
一元运算符 ! 显式地将值强制类型转换为布尔值。但是它同时还将真值反转为假值(或者将假值反转为真值)。所以显式强制类型转换为布尔值最常用的方法是 !!,因为第二个 ! 会将结果反转回原值
建议使用 Boolean(a) 和 !!a 来进行显式强制类型转换。
4、隐式强制类型转换
隐式强制类型转换指的是那些隐蔽的强制类型转换,隐式强制类型转换的作用是减少冗余,让代码更简洁
4.1、字符串与数组之间的隐式强制类型转换
var a = "42";
var b = "0";
var c = 42;
var d = 0;
a + b; // "420"
c + d; // 42
var a = [1,2];
var b = [3,4];
a + b; // "1,23,4"
简单来说就是,如果 + 的其中一个操作数是字符串,则执行字符串拼接;否则执行数字加法。
a + “”(隐式)和前面的 String(a)(显式)之间有一个细微的差别需要注意。根据ToPrimitive 抽象操作规则,a + “” 会对 a 调用 valueOf() 方法,然后通过 ToString 抽象操作将返回值转换为字符串。而 String(a) 则是直接调用 ToString()
var a = {
valueOf: function() { return 42; },
toString: function() { return 4; }
};
a + ""; // "42"
String( a ); // "4"
4.2、布尔值到数字的隐式强制类型转换
将true转换为1,false转换为0
4.3、隐式强制类型转换为布尔值
(1) if (…) 语句中的条件判断表达式。
(2) for ( … ; … ; … ) 语句中的条件判断表达式(第二个)。
(3) while (…) 和 do…while(…) 循环中的条件判断表达式。
(4) ? : 中的条件判断表达式。
(5) 逻辑运算符 ||(逻辑或)和 &&(逻辑与)左边的操作数(作为条件判断表达式)
以上情况中,非布尔值会被隐式强制类型转换为布尔值,遵循前面介绍过的 ToBoolean 抽象操作规则
var a = 42;
var b = "abc";
var c;
var d = null;
if (a) {
console.log( "yep" ); // yep
}
while (c) {
console.log( "nope, never runs" );
}
c = d ? a : b;
c; // "abc"
if ((a && d) || c) {
console.log( "yep" ); // yep
}
上例中的非布尔值会被隐式强制类型转换为布尔值以便执行条件判断。
4.4、|| 和 &&
在 JS中它们返回的并不是布尔值。它们的返回值是两个操作数中的一个(且仅一个)。即选择两个操作数中的一个,然后返回它的值。
&& 和 || 运算符的返回值并不一定是布尔类型,而是两个操作数其中一个的值。
var a = 42;
var b = "abc";
var c = null;
a || b; // 42
a && b; // "abc"
c || b; // "abc"
c && b; // null
对于 || 来说,如果条件判断结果为 true 就返回第一个操作数(a 和 c)的值,如果为false 就返回第二个操作数(b)的值。
&& 则相反,如果条件判断结果为 true 就返回第二个操作数(b)的值,如果为 false 就返回第一个操作数(a 和 c)的值。
下面是一个十分常见的 || 的用法,也许你已经用过但并未完全理解:
function foo(a,b) {
a = a || "hello";
b = b || "world";
console.log( a + " " + b );
}
foo(); // "hello world"
foo( "yeah", "yeah!" ); // "yeah yeah!"
a = a || “hello”(又称为 C# 的“空值合并运算符”的 JavaScript 版本)检查变量 a,如果还未赋值(或者为假值),就赋予它一个默认值(“hello”)。
再来看看 &&:
有一种用法对开发人员不常见,然而JS代码压缩工具常用。就是如果第一个操作数为真值,则 && 运算符“选择”第二个操作数作为返回值,这也叫作“守护运算符”,即前面的表达式为后面的表达式“把关”:
function foo() {
console.log( a );
}
var a = 42;
a && foo(); // 42
开发人员通常使用 if (a) { foo(); }。但 JavaScript代码压缩工具用的是 a && foo(),因为更简洁
4.5、符号的强制类型转换
ES6 中引入了符号类型,ES6 允许从符号到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误。
var s1 = Symbol( "cool" );
String( s1 ); // "Symbol(cool)"
var s2 = Symbol( "not cool" );
s2 + ""; // TypeError
符号不能够被强制类型转换为数字(显式和隐式都会产生错误),但可以被强制类型转换为布尔值(显式和隐式结果都是 true)。
5、宽松相等和严格相等
宽松相等(loose equals)== 和严格相等(strict equals)=== 都用来判断两个值是否“相等”,但是它们之间有一个很重要的区别,特别是在判断条件上
== 允许在相等比较中进行强制类型转换,而 === 不允许
5.1、抽象相等
几个特殊的情况:NaN不等于NaN,+0不等于-0
对象(包括函数和数组)的宽松相等 ==。两个对象指向同一个值时即视为相等,不发生强制类型转换
== 在比较两个不同类型的值时会发生隐式强制类型转换,会将其中之一或两者都转换为相同的类型后再进行比较
字符串与数字
var a = 42;
var b = "42";
a === b; // false
a == b; // true
(1) 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果。
(2) 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果。
其他类型与布尔值之间的相等比较
var a = "42";
var b = true;
a == b; // false
(1) 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果;
(2) 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果。
Type(x) 是布尔值,所以 ToNumber(x) 将 true 强制类型转换为 1,变成 1 == “42”,二者的类型仍然不同,“42” 根据规则被强制类型转换为 42,最后变成 1 == 42,结果为 false
反过来也一样,Type(y) 是布尔值,所以 ToNumber(y) 将 false 强制类型转换为 0,然后 “42” == 0 再变成42 == 0,结果为 false
但 “42” == true 中并没有发生布尔值的比较和强制类型转换。这里不是 “42” 转换为布尔值(true),而是 true 转换为 1,“42” 转换为 42
null与undefined
null 和 undefined 之间的 == 也涉及隐式强制类型转换
(1) 如果 x 为 null,y 为 undefined,则结果为 true。
(2) 如果 x 为 undefined,y 为 null,则结果为 true。
在 == 中 null 和 undefined 相等(它们也与其自身相等),除此之外其他值都不存在这种情况。这也就是说在 == 中 null 和 undefined 是一回事,可以相互进行隐式强制类型转换
对象与非对象之间
(1) 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果;
(2) 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPromitive(x) == y 的结果。
var a = 42;
var b = [ 42 ];
a == b; // true
[ 42 ] 首先调用 ToPromitive 抽象操作,返回 “42”,变成 “42” == 42,然后又变成 42 == 42,最后二者相等。
ToPromitive 抽象操作的所有特性(如 toString()、valueOf())在这里都适用
var a = null;
var b = Object( a ); // 和Object()一样
a == b; // false
var c = undefined;
var d = Object( c ); // 和Object()一样
c == d; // false
var e = NaN;
var f = Object( e ); // 和new Number( e )一样
e == f; // false
因为没有对应的封装对象,所以 null 和 undefined 不能够被封装(boxed),Object(null)和 Object() 均返回一个常规对象。
NaN 能够被封装为数字封装对象,但拆封之后 NaN == NaN 返回 false,因为 NaN 不等于 NaN
5.2、少见的情况
返回其他数字
Number.prototype.valueOf = function() {
return 3;
};
console.log(new Number( 2 ) == 3) // true
var i = 2;
Number.prototype.valueOf = function() {
return i++;
};
var a = new Number( 42 );
if (a == 2 && a == 3) {
console.log( "Yep, this happened." );
}
假值的相等比较
"0" == null; // false
"0" == undefined; // false
"0" == false; // true -- 晕!
"0" == NaN; // false
"0" == 0; // true
"0" == ""; // false
false == null; // false
false == undefined; // false
false == NaN; // false
false == 0; // true -- 晕!
false == ""; // true -- 晕!
false == []; // true -- 晕!
false == {}; // false
"" == null; // false
"" == undefined; // false
"" == NaN; // false
"" == 0; // true -- 晕!
"" == []; // true -- 晕!
"" == {}; // false
0 == null; // false
0 == undefined; // false
0 == NaN; // false
0 == []; // true -- 晕!
0 == {}; // false
比如我们都知道 “” 和 NaN 不相等,“0” 和 0 相等
极端情况
[] == ![] // true
0 == "\n"
根据 ToBoolean 规则,它会进行布尔值的显式强制类型转换(同时反转奇偶校验位)。所以 [] == ![] 变成了 [] == false。前面我们讲过 false == [],最后的结果就顺理成章了
“”、"\n"等空字符串被ToNumber强制类型转换为0
完整性检查
安全运用隐式强制转换
- 如果两边的值中有 true 或者 false,千万不要使用 ==
- 如果两边的值中有 []、“” 或者 0,尽量不要使用 ==
这时最好用 === 来避免不经意的强制类型转换。这两个原则可以让我们避开几乎所有强制类型转换的坑
6、抽象关系比较
a < b 中涉及的隐式强制类型转换不太引人注意,不过还是很有必要深入了解一下
该算法仅针对 a < b,a=“”> b 会被处理为 b <a
比较双方首先调用 ToPrimitive,如果结果出现非字符串,就根据 ToNumber 规则将双方强制类型转换为数字来进行比较
var a = [ 42 ];
var b = [ "43" ];
a < b; // true
b < a; // false
如果比较双方都是字符串,则按字母顺序来进行比较
var a = [ "42" ];
var b = [ "043" ];
a < b; // false
var a = [ 4, 2 ];
var b = [ 0, 4, 3 ];
a < b; // false
var a = { b: 42 };
var b = { b: 43 };
a < b; // false
// 因为 a 是 [object Object],b 也是 [object Object],所以按照字母顺序a < b 并不成立
a 和 b 并没有被转换为数字,因为 ToPrimitive 返回的是字符串,所以这里比较的是 "42"和 “043” 两个字符串,它们分别以 “4” 和 “0” 开头。因为 “0” 在字母顺序上小于 “4”,所以最后结果为 false。
下面的例子就有些奇怪了
var a = { b: 42 };
var b = { b: 43 };
a < b; // false
a == b; // false
a > b; // false
a <= b; // true
a >= b; // true
因为根据规范 a <= b 被处理为 b < a,然后将结果反转。因为 b < a 的结果是 false,所以 a <= b 的结果是 true
JavaScript 中 <= 是“不大于”的意思(即 !(a > b),处理为 !(b < a))。同理 a >= b 处理为 b <= a
7、总结
本章介绍了 JavaScript 的数据类型之间的转换,即强制类型转换:包括显式和隐式。
强制类型转换常常为人诟病,但实际上很多时候它们是非常有用的。作为有使命感的JavaScript 开发人员,我们有必要深入了解强制类型转换,这样就能取其精华,去其糟粕。
显式强制类型转换明确告诉我们哪里发生了类型转换,有助于提高代码可读性和可维护性。
隐式强制类型转换则没有那么明显,是其他操作的副作用。感觉上好像是显式强制类型转换的反面,实际上隐式强制类型转换也有助于提高代码的可读性。
在处理强制类型转换的时候要十分小心,尤其是隐式强制类型转换。在编码的时候,要知其然,还要知其所以然,并努力让代码清晰易读。