序言
值的类型转换是非常有争议的一点,因为这会产生一些问题,并且令人无法理解。但正因为如此,我们更要认识它,理解它。
值类型转换
JS中的类型转换统称:强制类型转换。其中区分“隐式强制类型转换”和“显示强制类型转换”
var a = 42;
var b = a + ""; // 隐式强制类型转换
var c = String( a ); // 显式强制类型转换
由于 +运算符的其中一个操作数是字符串,因此是字符串拼接的操作,这里发生了隐式强制。后者直接使用了String(),则是显示强制。
抽象值操作
-
ToString
ToString负责处理非字符串到字符串的强制类型转换,如果对象有自己的toString()方法,字符串化时会调用该方法并使用起返回值。
let a = 123;
a.toString() // '123'
let b = [1,2,3]
b.toString() // '1,2,3'
数字会直接返回该值的字符串形式,数组则会返回用“,”拼接的结果。它可以主动调用,也可以内部自动调用。
JSON 字符串化
工具函数 JSON.stringify(..) 在将 JSON 对象序列化为字符串时也用到了 ToString。虽然JSON 字符串化并非严格意义上的强制类型转换。它只是部分转换机制和 toString()相同。
所有安全的JSON值都可以使用JSON.stringify()字符串化。那么什么是不安全的呢,就是undefined、function、symbol和包含循环引用的对象都不符合JSON结构标准。
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(..) 在对象中遇到 undefined、function 和 symbol 时会自动将其忽略,在 数组中则会返回 null(以保证单元位置不变)。如果对象中定义了 toJSON() 方法,JSON 字符串化时会首先调用该方法,然后用它的返回 值来进行序列化。
var a = {
val: [1,2,3],
toJSON: function(){
return this.val.slice( 1 );
}
};
JSON.stringify( a ); // "[2,3]"
toJSON() 应该是“返回一个能够被字符串化的安全的 JSON 值”。当然需要我们来定义它的返回
JSON.stringify(..) 还可以传递一个可选参数 replacer,它可以是数组或者函数,用来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除。
如果是数组则必须是字符串数组,其中包含要被处理的对象,除此之外的其他属性将被忽略。
如何是函数,它会先调用对象一次,然后它会对对象中的各个属性调用一次,每次带两个参数,分别是键和值
var a = {
b: 42,
c: "42",
d: [1,2,3]
};
JSON.stringify( a, ["b","c"] ); // "{"b":42,"c":"42"}"
JSON.stringify( a, function(k,v){
if (k !== "c") return v;
} );
// "{"b":42,"d":[1,2,3]}"
JSON.string 还有一个可选参数 space,用来指定输出的缩进格式。space 为正整数时是指 每一级缩进的字符数,它还可以是字符串,此时最前面的十个字符被用于每一级的缩进:
var a = {
b: 42,
c: "42",
d: [1,2,3]
};
JSON.stringify( a, null, 3 );
// "{
//
//
//
//
//
// 3
// ]
// }"
- ToNumber ToNumber的转换规则:
- 是true 转换为 1,false 转换为 0。undefined 转换为 NaN,null 转换为 0。
- 对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。
为了将值转换为基本类型,抽象操作 ToPrimitive会有几点操作
- 检查该值是否有 valueOf() 方法,如果有并且返回基本类型值,就使用该值进行强制类型转换
- 如果没有就使用 toString()的返回值(如果存在)来进行强制类型转换。
- 如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。
var a = {
valueOf: function(){
return "42";
}
};
var b = {
toString: function(){
return "42";
}
};
var c = [4,2];
c.toString = function(){
return this.join( "" ); // "42"
};
Number( a ); // 42
Number( b ); // 42
Number( c ); // 42
Number( "" ); // 0
Number( [] ); // 0
Number( [ "abc" ] );// NaN
- ToBoolean JS 中有两个关键词 true 和 false,分别代表布尔类型中的真和假,我们常认为1和0分别代表true和false,但是实际上并不是这样的。
- 假值(falsy value)
JavaScript 中的值可以分为以下两类:
(1) 可以被强制类型转换为 false 的值
(2) 其他(被强制类型转换为 true 的值)
ES5 规范 9.2 节中定义了抽象操作 ToBoolean,列举了布尔强制类型转换所有可能出现的结果。
以下这些是假值:
-
undefined
-
null
-
false
-
+0、-0 和 NaN
-
"" 假值的布尔强制类型转换结果为 false。
-
真值(truthy value) 这当然是除假值之外的值,像[],{},function(){},等都是真值,因为它们不在假值列表里面。
显示强制类型转换
- 字符串和数字之间的显示转换 这一转换在实际中还是比较常见的,比较直接的是使用Number()和String()直接转换。
var a = 42;
var b = String( a );
var c = "3.14";
var d = Number( c );
另外,一元运算符 +,-。
let a = '3.14';
let b = +a //3.14
另外 ~ 运算符也可以,它首先将值强制类型转换为 32 位数字,然后执行字位操作“非”(对每一个字位进行反转)。说白点,~ 返回 2 的补码,~x 大致等同于 -(x+1),如:~42 ==> -43
~ 和indexof结合可以用来判断某字符是否存在,更简洁。
var a = "Hello World";
if (~a.indexOf( "lo" )) { // ~a.indexOf( "lo" ) 等于 -4 是真值
}
~a.indexOf( "ol" ); // indexOf( "ol" )找不到返回-1,这样整个值就是 0 ==>假值,取反变真
if (!~a.indexOf( "ol" )) { // true 没有找到匹配!
}
解析数字字符串
解析允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停 止。而转换不允许出现非数字字符,否则会失败并返回 NaN。
let a = '42px'
Number(a) // NaN
parseInt(a) //42
parseInt的第二个参数是指转换的基数,如2,8,16等进制,ES5开始默认是10进制。来看个例子
parseInt( 1/0, 19 ); // 18
是不是懵了,为啥啊,不应该是 Infinity么,简直离谱。
我们来看看它是怎么运行的,1/0有两种选择,"Infinity" 和 "∞",显然JS选择的是Infinity,不然后续就报错了。所以上式解析成parseInt( "Infinity", 19 ),那么按19进制来看的话,第一个数值 I 在19进制里面是有效数值,(19进制:0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F,G,H,I),而后续的 n 在19进制里面是无效的,由于parseInt的解析是遇到非进制数字的字符就停止了,故只取I,故最后结果表示为10进制的 18。
- 显式转换为布尔值 Boolean和 !运算符都能显示的将值强制转换成布尔值。!!则会将值反转为原值。常见的三目运算符也是显示的强制类型转换,只不过,中间掺杂了的隐式强制类型转换,需要注意。
隐式强制类型转换
隐式强制类型转换指的是那些隐蔽的强制类型转换,副作用也不是很明显。但是这转换起来会让代码变得晦涩难懂。当然有好有坏,我们应该做的是取其精华。
-
字符串和数字之间的隐式强制类型转换
之前我们知道,+运算符能用于数字加法,也能用于字符串拼接,那如何知道是什么操作呢。
这就得按照规范来看了,如果某操作数能够通过以下步骤转换得到字符串,将进行拼接操作。如果其中一个是对象,则首先调用ToPrimitive 抽象操作,该抽象操作再调用[[DefaultValue]],以数字为上下文。
var a = [1,2];
var b = [3,4];
a + b; // "1,23,4"
这里的数组valueOf操作无法得到基本类型字符,故而使用ToString,两个数组变成了了1,2和3,4故结果为1,23,4。
简单来说就是,如果 + 的其中一个操作数是字符串(或者通过以上步骤可以得到字符串), 则执行字符串拼接;否则执行数字加法。
数字可以通过和空字符串" " 相+ 来将其转换成字符串
var a = 42;
var b = a + "";
b; // "42"
这种 a+"" 和之前 String() 有一个区别是,a + ""会对a调用valueOf()方法,然后通过ToString抽象 操作将返回值转换为字符串。而 String(a) 则是直接调用 ToString()。
-,*,/ 等运算符则是直接执行了数学运算,他们会被转换成数字再进行操作。
- 布尔值到数字的隐式强制类型转换 布尔值遇到 + 时,true会变成1,false变成0。
true + true //2
true + false //1
//这里都是隐式转换
- 隐式强制类型转换为布尔值 下面的情况会发生 布尔值隐式强制类型转换。
- if (..)语句中的条件判断表达式。
- for ( .. ; .. ; .. )语句中的条件判断表达式(第二个)。
- while (..) 和 do..while(..) 循环中的条件判断表达式。
- ? :中的条件判断表达式。
- 逻辑运算符 ||(逻辑或)和 &&(逻辑与)左边的操作数(作为条件判断表达式)。
||和&&,一般情况下是可以作为转换布尔值来使用,但是正确饿理解是&& 和 || 运算符的返回值并不一定是布尔类型,而是两个操作数其中一个的值。
var a = 42;
var b = "abc";
a || b // 42
a && b // "abc"
||和&&会对第一个操作数进行判断,如果其不是布尔值就先进行ToBoolean 强制类型转换,然后再执行条件判断。这是区别于其他语言的,其他语言可能直接返回两者转换后的布尔值。
ES6的Symbol是不能转换为字符串或者数字的,会产生错误,但是可以转换成布尔值,而且结果都是true。
宽松相等和严格相等
=== 和 == 想必大家都常见,但是对于其中的解释是否理解过,这里给出的解释是:” == 允许在相等比较中进行强制类型转换,而 === 不允许。”所以这种解释就意味着 == 的工作量更大,因为值的类型不同的话需要进行强制类型转换。
var a = 42;
var b = "42";
a === b; // false
a == b; // true
a==b 是宽松相等的,那么是是 a 从 42 转换为字符串,还是 b 从 "42" 转换为数字?
ES5 规范 11.9.3.4-5 这样定义:
如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果。如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果。也就是都会转换成数字进行比较。
而其他类型和布尔类型的对比,尤为容易出错
var a = "42";
var b = true;
a == b; // false
规范 11.9.3.6-7 是这样说的:
如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果;如果 Type(y) 是布尔类型,则返 x == ToNumber(y) 的结果。
因此,布尔值和数字进行对比时,布尔值都会转换成1 或者0。
那就有一个疑惑了,42 == true是错的,42 == false也是错的,那是不是就代表42即不是真值也不是假值了么。实际并不是这样的,因为这里的比较中,没有发生布尔值的强制转换,都是布尔值转换成数字类型,不涉及ToBoolean,所以 "42" 是真值还是假值与 == 本身没有关系!
什么情况下都不要使用 == true 和 == false。
null 和 undefined 之间的 == 也涉及隐式强制类型转换。ES5 规范 11.9.3.2-3 规定:
如果 x 为 null,y 为 undefined,则结果为 true。如果 x 为 undefined,y 为 null,则结果为 true。也就是 null == undefined,它们也与其自身相等,除此外没有其他值能相等。
关于对象和非对象之间的相等比较,ES5 规范 11.9.3.8-9 做如下规定:
如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPromitive(x) == y 的结果ToPrimitive 就转成基本类型的说法。布尔值会事先转换成数字再进行转换。
le a = [42]
let b = 42
a == b //true a先转换成'42' 成为 '42' == 42 后再变成 42 == 42
var a = "abc";
var b = Object( a );
a == b //true b通过ToPrimitive进行强制类型换行的值为'abc',与a相等
但是也有一些例外场景
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 不能被封装,所以返回一个常规对象{},而NaN虽然能被封装,但是NaN 不等于NaN。
来看看其他有趣的例子
如何让 if (a == 2 && a == 3) {}执行呢,或许你觉得不可能,但是 && 不代表同时,a==2实际上是在a==3之前执行的。
var i = 2;
Number.prototype.valueOf = function() {
return i++;
};
var a = new Number( 42 );
if (a == 2 && a == 3) {
console.log( "Yep, this happened." );
}
利用a.valueOf产生的副作用,然后Number()进行比较时会发生强制类型转换,因此调用valueOf产生值的变化。
假值的相等比较
== 中的隐式强制类型转换最为人诟病的地方是假值的相等比较。
"0" == false; // true
false == 0; // true
false == "";// true
false == [];// true
"" == 0;// true
"" == [];// true
0 == [];// true
这里列出了7中特殊的假值比较,它们这是属于假阳的情况。== false 的情况好理解,因为false 会被转换成 0 再进行比较,然后 Number('')或者Number([])的结果都是0。
而 '' == [],遵循上述的对象转换规则来说实际是 '' == ''([].toString),结果也就相等了。
还有更奇怪的
[] == ![] //true
来看看具体发生了什么吧,根据ToBoolean规则,会先进行布尔值的强制类型转换,变成
[] == false
然后这个结果就和前面的特殊情况一样了,再进行数字转换,0 == 0。
"" == [null]; // true [null].toString() ==> " "
0 == "\n"; // true
""、"\n"、或者其他空格组合字符串被ToNumber 强制类型转换为0,结果就是 0 == 0
所以,得出的结论是
- 如果两边的值中有 true 或者 false,千万不要使用 ==。
- 如果两边的值中有 []、"" 或者 0,尽量不要使用 ==。 因为你确实不知道什么时候,这些转换会出问题。可以的话,尽量使用 === 吧
抽象关系比较
a < b 这种比较方式也需要认识一下。ES5 规范 11.8.5 节定义了“抽象关系比较” 分为两个部分:比较双方都是字符串和其他情况。
比较双方首先调用 ToPrimitive,如果结果出现非字符串,就根据 ToNumber 规则将双方强 制类型转换为数字来进行比较。
var a = [ 42 ];
var b = [ "43" ];
a < b //true
==> 42 < '43' ==> 42 < 43
如果比较双方都是字符串,则按字母顺序来进行比较:
var a = [ "42" ];
var b = [ "043" ];
a < b; // false
==> '42' < '043'
0在字母的顺序上小于4,所以结果是false。
var a = [ 4, 2 ];
var b = [ 0, 4, 3 ];
a < b; // false 和上面一样 '4,2' > '0,4,3'
var a = { b: 42 };
var b = { b: 43 };
a < b //false
==> [object Object] < [object Object],所以小于不成立。
再来看
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本质上是指向两个不同的存储空间的,所以是不相等的。而根据规范a <= b被处理为b < a,然后将结果反转。因为 b < a 是false,所以反转就是true,a<=b为true
所以 <=在JS中是实际上表示的是 “不大于”的意思,即 !(a>=b),处理为 !(b<=a)
这个比较时有点绕,需要多理解一下。比较关系没有严格关系的比较,如果要避免两者发生隐式类型转换,就必须保证两者类型是一致的。