js类型转换知识总结

显示强制类型转换

显式强制类型转换 是指那些显而易见的类型转换

字符串和数字之间的显示转换

通过String(..)Number(..)两种方法来实现

1
2
3
4
5
6
7
8
var a = 42;
var b = String( a );

var c = "3.14";
var d = Number( c );

b; // "42"
d; // 3.14

除了 String(..)Number(..) 以外,还可以通过toString(..)和三元符号+来实现显示转换

1
2
3
4
5
6
7
8
9
var a = 42;
var b = a.toString();

var c = "3.14";
var d = +c;
var e =+ c;
b; // "42"
d; // 3.14
e; // 3.14

日期显式转换为数字

1
2
3
var d = new Date( "Wed Nov 16 2022 21:17:41 GMT+0800" );

+d; // 1668604661000

获取当前时间戳

1
2
var timestamp = +new Date();
timestamp; // 1668604760341

~ 运算符

~ 运算符,(即字位操作“非”),~x 大致等同于 -(x+1)

1
~42;    // -(42+1) ==> -43

显式解析数字字符串

1
2
3
4
5
6
7
8
var a = "42";
var b = "42px";

Number( a ); // 42
parseInt( a ); // 42

Number( b ); // NaN
parseInt( b ); // 42

解析允许 字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停止。而转换不允许 出现非数字字符,否则会失败并返回 NaN 。

解析非字符串

1
2
1/0; // Infinity 
parseInt( 1/0, 19 ); // 18

parseInt(1/0, 19) 实际上是 parseInt(“Infinity”, 19) 。第一个字符是 “I” ,以 19 为基数时值为 18 。第二个字符 “n” 不是一个有效的数字字符,解析到此为止,和 “42px” 中的 “p” 一样。

1
2
3
4
5
6
7
parseInt( new String( "42") ); // 42
var a = {
num: 21,
toString: function() { return String( this.num * 2 ); }
};

parseInt( a ); // 42

先将参数强制类型转换为字符串再进行解析,这样做没有任何问题。因为传递错误的参数而得到错误的结果,并不能归咎于函数本身。

还有一些奇怪的现象

1
2
3
4
5
6
7
parseInt( 0.000008 );       // 0   ("0" 来自于 "0.000008")
parseInt( 0.0000008 ); // 8 ("8" 来自于 "8e-7")
parseInt( false, 16 ); // 250 ("fa" 来自于 "false")
parseInt( parseInt, 16 ); // 15 ("f" 来自于 "function..")

parseInt( "0x10" ); // 16
parseInt( "103", 2 ); // 2

显式转换为布尔值

Boolean(..) (不带 new )是显式的 ToBoolean 强制类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var a = "0";
var b = [];
var c = {};

var d = "";
var e = 0;
var f = null;
var g;

!!a; // true
!!b; // true
!!c; // true

!!d; // false
!!e; // false
!!f; // false
!!g; // false

+ 类似,一元运算符 ! 显式地将值强制类型转换为布尔值。但是它同时还将真值反转为假值(或者将假值反转为真值)。

if(..).. 这样的布尔值上下文中,如果没有使用 Boolean(..)!! ,就会自动隐式地进行 ToBoolean 转换

ToBoolean 的另外一个用处,是在 JSON 序列化过程中将值强制类型转换为 true 或 false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var a = [
1,
function(){ /*..*/ },
2,
function(){ /*..*/ }
];

JSON.stringify( a ); // "[1,null,2,null]"

JSON.stringify( a, function(key,val){
if (typeof val == "function") {
// 函数的ToBoolean强制类型转换
return !!val;
}
else {
return val;
}
} );
// "[1,true,2,true]"

隐式强制类型转换

隐式强制类型转换 指的是那些隐蔽的强制类型转换,副作用也不是很明显

字符串和数字之间的隐式强制类型转换

+ 运算符即能用于数字加法,也能用于字符串拼接

1
2
3
4
5
6
7
8
var a = "42";
var b = "0";

var c = 42;
var d = 0;

a + b; // "420"
c + d; // 42

如果 + 的其中一个操作数是字符串(或者通过以上步骤可以得到字符串),则执行字符串拼接;否则执行数字加法

1
2
3
4
5
6
7
8
var a = {
valueOf: function() { return 42; },
toString: function() { return 4; }
};

a + ""; // "42"

String( a ); // "4"

a + “” (隐式)和前面的 String(a) (显式)之间有一个细微的差别需要注意。根据 ToPrimitive 抽象操作规则,a + “” 会对 a 调用 valueOf() 方法,然后通过 ToString 抽象操作将返回值转换为字符串。而 String(a) 则是直接调用 ToString() 。

1
2
3
4
var a = "3.14";
var b = a - 0;

b; // 3.14

- 是数字减法运算符,因此 a - 0 会将 a 强制类型转换为数字

布尔值到数字的隐式强制类型转换

应用场景

1
2
3
4
5
6
7
8
9
10
11
12
function onlyOne(a,b,c) {
return !!((a && !b && !c) ||
(!a && b && !c) || (!a && !b && c));
}

var a = true;
var b = false;

onlyOne( a, b, b ); // true
onlyOne( b, a, b ); // true

onlyOne( a, b, a ); // false

这里代码阅读性很差,如果差数多起来就更加不好维护,这里改用另外一种方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function onlyOne() {
var sum = 0;
for (var i=0; i < arguments.length; i++) {
// 跳过假值,和处理0一样,但是避免了NaN
if (arguments[i]) {
sum += arguments[i];
}
}
return sum == 1;
}

var a = true;
var b = false;

onlyOne( b, a ); // true
onlyOne( b, a, b, b, b ); // true

onlyOne( b, b ); // false
onlyOne( b, a, b, b, b, a ); // false

也可以修改成如下,代码阅读性更好

1
2
3
4
5
6
7
function onlyOne() {
var sum = 0;
for (var i=0; i < arguments.length; i++) {
sum += Number( !!arguments[i] );
}
return sum === 1;
}

隐式强制类型转换为布尔值

下面的情况会发生布尔值隐式强制类型转换

(1) if (..) 语句中的条件判断表达式。

(2) for ( .. ; .. ; .. ) 语句中的条件判断表达式(第二个)。

(3) while (..) 和 do..while(..) 循环中的条件判断表达式。

(4) ? : 中的条件判断表达式。

(5) 逻辑运算符 || (逻辑或)和 && (逻辑与)左边的操作数(作为条件判断表达式)。

1
if (a || b) ....      if (a && b) ....

|| 和 &&

|| 和 &&叫做“逻辑运算符”,因为这不太准确。称它们为“选择器运算符”

1
2
3
4
5
6
7
8
9
var a = 42;
var b = "abc";
var c = null;

a || b; // 42
a && b; // "abc"

c || b; // "abc"
c && b; // null

||&& 首先会对第一个操作数 (a 和 c )执行条件判断,如果其不是布尔值(如上例)就先进行 ToBoolean 强制类型转换,然后再执行条件判断。

对于 || 来说,如果条件判断结果为 true 就返回第一个操作数(a 和 c )的值,如果为 false 就返回第二个操作数(b )的值。

&& 则相反,如果条件判断结果为 true 就返回第二个操作数(b )的值,如果为 false 就返回第一个操作数(a 和 c )的值。

有一种用法对开发人员不常见,然而 JavaScript 代码压缩工具常用。就是如果第一个操作数为真值,则 && 运算符“选择”第二个操作数作为返回值,这也叫作“守护运算符”

1
2
3
4
5
6
7
function foo() {
console.log( a );
}

var a = 42;

a && foo(); // 42

foo() 只有在条件判断 a 通过时才会被调用。如果条件判断未通过,a && foo() 就会悄然终止(也叫作“短路”,short circuiting),foo() 不会被调用。

|| 和 && 在if语句

1
2
3
4
5
6
7
var a = 42;
var b = null;
var c = "foo";

if (a && (b || c)) {
console.log( "yep" );
}

这里 a && (b || c) 的结果实际上是 “foo” 而非 true ,然后再由 if 将 foo 强制类型转换为布尔值,所以最后结果为 true 。

符号的强制类型转换

1
2
3
4
5
var s1 = Symbol( "cool" );
String( s1 ); // "Symbol(cool)"

var s2 = Symbol( "not cool" );
s2 + ""; // TypeError

符号不能够被强制类型转换为数字(显式和隐式都会产生错误),但可以被强制类型转换为布尔值(显式和隐式结果都是 true )。

宽松相等和严格相等

宽松相等(loose equals)== 和严格相等(strict equals)=== 都用来判断两个值是否“相等”,但是它们之间有一个很重要的区别,特别是在判断条件上。

== 允许在相等比较中进行强制类型转换,而 === 不允许。

1
2
3
4
var a = 1;
var b = '1';
a == b; // true
a === b; // false

相等比较操作的性能

=====仅仅是微秒级(百万分之一秒)的差别而已

抽象相等

1
2
3
4
NaN == NaN; // false
NaN === NaN; // false
+0 == -0; // true
+0 === -0; // true

字符串和数字之间的相等比较

1
2
3
4
5
var a = 42;
var b = "42";

a === b; // false
a == b; // true

因为没有强制类型转换,所以 a === b 为 false ,42 和 “42” 不相等。

而 a == b 是宽松相等,即如果两个值的类型不同,则对其中之一或两者都进行强制类型转换。

具体怎么转换?是 a 从 42 转换为字符串,还是 b 从 “42” 转换为数字?

(1) 如果 Type(a) 是数字,Type(b) 是字符串,则返回 a == ToNumber(b) 的结果。

(2) 如果 Type(a) 是字符串,Type(b) 是数字,则返回 ToNumber(a) == b 的结果。

其他类型和布尔类型之间的相等比较

1
2
3
4
var a = "42";
var b = true;

a == b; // false

(1) 如果 Type(a) 是布尔类型,则返回 ToNumber(a) == b 的结果;

(2) 如果 Type(b) 是布尔类型,则返回 a == ToNumber(b) 的结果。

Type(x) 是布尔值,所以 ToNumber(x) 将 true 强制类型转换为 1 ,变成 1 == “42” ,二者的类型仍然不同,”42” 根据规则被强制类型转换为 42 ,最后变成 1 == 42 ,结果为 false 。

Type(y) 是布尔值,所以 ToNumber(y) 将 false 强制类型转换为 0 ,然后 “42” == 0 再变成 42 == 0 ,结果为 false 。

代码中不要使用 == true 和 == false

null 和 undefined 之间的相等比较

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = null;
var b;

a == b; // true
a == null; // true
b == null; // true

a == false; // false
b == false; // false
a == ""; // false
b == ""; // false
a == 0; // false
b == 0; // false

(1) 如果 x 为 null ,y 为 undefined ,则结果为 true 。

(2) 如果 x 为 undefined ,y 为 null ,则结果为 true 。

在 == 中 null 和 undefined 相等(它们也与其自身相等),除此之外其他值都不存在这种情况。

对象和非对象之间的相等比较

1
2
3
4
5
var a = 42;
var b = [ 42 ];

a == b; // true
a === b; // false

(1) 如果 Type(a) 是字符串或数字,Type(y) 是对象,则返回 a == ToPrimitive(b) 的结果;

(2) 如果 Type(a) 是对象,Type(b) 是字符串或数字,则返回 ToPromitive(a) == b 的结果。

input Type Result
Undefined input argument
Null input argument
Boolean input argument
Number input argument
String input argument
Object 忽略 第二个参数 hint PreferredType 直接调用内置方法 [[DefaultValue]]
1
2
3
4
5
var a = "abc";
var b = Object( a ); // 和new String( a )一样

a === b; // false
a == b; // true

a == b 结果为 true ,因为 b 通过 ToPromitive 进行强制类型转换(也称为“拆封”,英文为 unboxed 或者 unwrapped),并返回标量基本类型值 “abc” ,与 a 相等。

也有例外的情况

1
2
3
4
5
6
7
8
9
10
11
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

比较少见的情况

返回其他数字

1
2
3
4
5
Number.prototype.valueOf = function() {
return 3;
};

new Number( 2 ) == 3; // true

如果让 a.valueOf() 每次调用都产生副作用,比如第一次返回 2 ,第二次返回 3 ,就会出现这样的情况

1
2
3
4
5
6
7
8
9
10
11
var i = 2;

Number.prototype.valueOf = function() {
return i++;
};

var a = new Number( 42 );

if (a == 2 && a == 3) {
console.log( "Yep, this happened." );
}

假值的相等比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
"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

然而有 7 种我们注释了“晕!”,因为它们属于假阳(false positive)的情况,里面坑很多。

极端情况

1
[] == ![]   // true

根据 ToBoolean 规则,它会进行布尔值的显式强制类型转换(同时反转奇偶校验位)。所以 [] == ![] 变成了 [] == false 。前面我们讲过 false == []

1
2
2 == [2];       // true
"" == [null]; // true

== 右边的值 [2] 和 [null] 会进行 ToPrimitive 强制类型转换,以便能够和左边的基本类型值(2 和 “” )进行比较。因为数组的 valueOf() 返回数组本身,所以强制类型转换过程中数组会进行字符串化。

第一行中的 [2] 会转换为 “2” ,然后通过 ToNumber 转换为 2 。第二行中的 [null] 会直接转换为 “” 。

所以最后的结果就是 2 == 2 和 “” == “”

1
0 == "\n";  // true

“” 、”\n” (或者 “ “ 等其他空格组合)等空字符串被 ToNumber 强制类型转换为 0

完整性检查

1
2
3
4
5
6
7
"0" == false;          // true -- 晕!避免使用!!!!!!
false == 0; // true -- 晕!避免使用!!!!!!
false == ""; // true -- 晕!避免使用!!!!!!
false == []; // true -- 晕!避免使用!!!!!!
"" == 0; // true -- 晕!
"" == []; // true -- 晕!
0 == []; // true -- 晕!

抽象关系比较

比较双方首先调用 ToPrimitive ,如果结果出现非字符串,就根据 ToNumber 规则将双方强制类型转换为数字来进行比较

1
2
3
4
5
var a = [ 42 ];
var b = [ "43" ];

a < b; // true
b < a; // false

如果比较双方都是字符串,则按字母顺序来进行比较

1
2
3
4
var a = [ "42" ];
var b = [ "043" ];

a < b; // false

a 和 b 并没有被转换为数字,因为 ToPrimitive 返回的是字符串,所以这里比较的是 “42” 和 “043” 两个字符串,它们分别以 “4” 和 “0” 开头。因为 “0” 在字母顺序上小于 “4” ,所以最后结果为 false

1
2
3
4
5
var a = { b: 42 };
var b = { b: 43 };

a < b; // false
a == b; // true

a 是 [object Object] ,b 也是 [object Object] ,按照字母顺序 a < b 并不成立