JavaScript的类型转换你真的懂吗?

555 阅读13分钟

1. 值类型转换

什么是值类型转换?

值类型转换有两种,一种是显式类型转换:就是将值从一种类型转换为另一种类型,通常称为类型转换。一种是隐式类型转换,被称为强制类型转换。

二者的区别是显而易见的,可以从代码中直接看出来那些类型转换是显式类型转换,那些类型转换是隐式类型转换

var a = 12; 
var b = a + "";   //隐式强制类型转换
var c = String(a);  //显式强制类型转换

对变量b来说,由于+运算符的其中一个操作数是字符串,所以是字符串拼接操作,结果数字12被拼接为了字符串"12",所以可以说b的强制类型转换是隐式的;

对于c来说,是通过String()将a显式强制类型转换为了字符串,所以可以说变量c的强制类型转换是显式的。

有人说隐式强制类型转换的弊端大于显式强制类型转换,因为隐式强制类型转换有时候会让开发者摸不着头脑。

但是显式和隐式都是相对而言的,如果你明白字符串拼接,那么a + "" 就是显式的,如果你不了解String()可以做字符串强制类型转换,那么它对于你来说就是隐式的。

有位名人说过,我们编写的代码是给开发者看的,顺便运行在了程序中。

2. 抽象值操作

在讨论显式和隐式强制类型转换之前,我们需要先了解一下字符串、数字、布尔值之间类型转换的规则

ToString

ToString负责处理费字符串到字符串的强制类型转换。

基本类型之的字符串化规则为: null转换为"null",undefined转换为"undefined",true转换为"true"。

可以查看ESMA规范中的9.8节:

对普通对象来说,除非是自行定义,否则tostring()返回内部属性[[Class]]的值,如"[object Object]"

而数组比较例外,数组默认的toString()方法经过了重新定义,将所有单元字符串化以后再用",",链接起来,类似于join(",");

var arr = [1,2,3];
a.toString();  //"1,2,3"
arr.join(","); //"1,2,3"

toString()可以被显式调用,或者在需要字符串化时自动调用。

JSON字符串化

工具函数JSON.stringify()在将JSON对象序列化为字符串时也用到了ToString()。对大多数简单值来说,JSON字符串化和toString()的效果基本相同,不同的是JSON.stringify()序列化的结果总是字符串。 例如

JSON.stingify(42);  //"42"
SON.stingify("42");  //""42"" (对字符串序列化的时候,会生成一个含有双引号的字符串)
SON.stingify(null);  //"null"
SON.stingify(false);  //"false"

但是JSON.stringify()并不是强制类型转换,谈到它是因为它涉及到了ToString();

ToNumber

有时候我们需要将非数字当做数字使用。

基本类型的数字转换则为: true转换为了1,false转换为了0,undefined转换为了NaN,null转换为0。

可以查看ESMA规范中的9.3节:

对象(包括数组)会首先被转换为响应的基本类型之,如果返回的是非数字的基本类型值,则遵循上边的规则将其强制转换为数字。

为了将值转换为相应的基本类型值,会首先调用ToPrimitive检查该值是否有valueOf()方法,如果有就返回基本类型值,使用该值进行强制类型转换。如果没有的话就使用toSting()的返回值进行强制类型转换,如果二者都不存在,则产生TyoeError错误。但是要注意的一点是在从ES5开始,使用Object.create(null)创建对象的[[Prototype]]的属性为null,所以是没有valueOf()和toString()方法的,所以是没有办法进行强制类型转换的。 例如

var a = {
    valueOf: function() {
        retrun "12"
    }
};
var b = {
    toString: function() {
        return: "12";
    }
}
var c = [1,2];
c.toString = function() {
    return this.join("")
}

Number(a);  //12
Number(b); //12
Number(c); //12
Number(""); //0
Number([]); //0
Number(["abcd"]); //NaN

ToBoolean

我们知道在JavaScript中有两个常用的关键词true和false,代表了布尔类型的真值和假值。而在比较的时候我们常误以为true和false等同于1和0,虽然我们可以将1强制转换为true,将0强制转换为false,反之亦可,但是在JavaSript中布尔值和数字是不一样的。

参考ESMA规范

JavaScript中的值可以分为两类

(1)可以被强制类型转换为false的值

(2)可以被强制类型转换为true的值(其他) 因为可以被强制类型转换为true的值太多了,但是可以被转换为false的值缺只有一小部分,我们只要记住哪些值是假值就可以区分出真值和假值了。

以下这些是假值

null
undefined
false
+0、-0和NaN
""

从逻辑上理解,假值以外的都应该是真值。

3.显式强制类型转换

所谓的显式强制类型转换就是显而易见的类型转换。

var a = 12;
var b = String(a);
console.log(b)  //"12"

var c = "66.88"
var d = Number(c);
console.log(d);  //66.88

以上是字符串和数字之间的转换,通过String()和Number()这两个内建函数实现。String()遵循ToString()规则,Number()遵循ToNumber()规则,将值转换为基本类型。

除此之外还有其他方法可以实现数字和字符串之间的显式转换:

var a = 12;
var b = a.toString();
console.log(b);  //"12"

var c = "66.88";
var d = +c;
console.log(d); //66.88

a.toString()是显式的,但是其中包含了隐式转换,因为基本类型值12是没有toString()方法的,所以需要先对数字12进行封装,然后再调用toString()方法,而+c则是利用了+一元运算符可以将c强制转换为数字。在项目中使用+一元运算符将日期显式转换为数字时间戳也是很常见的一种使用方式。 例如:

var d = new Date("Sat Apr 27 2019 18:46:56 GMT+0800 (中国标准时间)")
+d;   //1556362016000

当然将日期显式转换为时间戳还有其他方式:

var d = new Date().getTime(); //1556362150329
var d1 = Date.now();  //1556362186672

当然这几种方法中是有一些细微区别的,+d是将时间戳的毫秒都改为了000的数值,不是很精确,而new Date().getTime()可以获得指定时间的时间戳,Date.now()可以获得当前时间的时间戳,可以根据需要快速的获取所需要的时间戳。

在很多时候我们需要将"100px"转换为数字100,这个时候我们使用转换就不行了,需要用到解析,虽然解析和转换都是将数字字符串转换为数字,但是二者是有区别的,转换只能转换字符串中只包含数字的字符,但是解析可以解析字符串中包含非数字字符,解析我们可以用parseInt()方法,例如:

var a = "100";
var b = "100px";
Number(a);  //100
parseInt(a);  //100
Number(b);  //NaN
parseInt(b); //100

parseInt()方法只能解析字符串值,传递其他参数是无用的,例如数字,布尔值(true),对象([1,2,3])等等;

说完了字符串和数字之间的转换,下面来看一下从非布尔值转换为布尔值。

var a = [];
var b = {};
var c = "0";
var d = 0;
var e = "";
var f = null;
var g = undefined;
var h;
Boolean(a);  //true
Boolean(b);  //true
Boolean(c);  //true
Boolean(d);  //false
Boolean(e);  //false
Boolean(f);  //false
Boolean(g);  //false
Boolean(h);  //false 
!!c //true
!!d //false

看一下是不是和我们前边说的是一样的,少数假值之外,其他的都是真值,虽然Boolean()是显式转换,但是在日常项目中并不常用, 更常用的是!符号,而在if() {}判断语句中,如果没有使用Boolean()和!符号,那么会自动进行隐式转换,这也是为什么在项目中可以直接用if判断是否符合条件的原因。但是建议使用Boolean()和!!进行显式转换,这样可以让代码具有更好的可读性。

4.隐式类型转换

隐式类型转换指的是那些隐蔽的强制类型转换,隐式强制类型转换可以使代码更为简洁,减少冗余代码。隐式类型转换没有明确的转换方式,只要你自己觉得不是明显的显式强制类型转换,那么你都可以认为它是隐式强制类型转换。

字符串和数字两者之间的隐式类型强制转换,例如:

var a = '100';
var b = '0';

var c = 100;
var d = 0;
a + b;   //'1000'
c + d;    //100
a + c;  //'100100'
b + d;   //'00'

再看对象(数组)之间的隐式类型转换:

var a = [1,2];
var b = [3,4];
a + b; //'1,23,4'

从以上例子我们可以看出来,如果某个操作数是字符串的话,将会使用+进行拼接操作,如果两者都是数字的话,则会使用数字运算进行加法运算。而如果其中一个操作数是对象(数组)的话,则会对该操作数调用ToPrimitive抽象操作。

进行ToPrimitive抽象操作的时候,会调用valueOf()方法,如果调用valueOf()方法无法得到简单基本类型值,就会转而调用toString()方法,反之亦然。

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

一般情况下,我们都是将其他类型向布尔值转换,但是有时候使用布尔值向数字转换也会有使用场景。

例如:

var a = true;
var b = false;
a + b;  //1
a + a; //2
b + b; //0

通过以上例子可以看出来,在转换的时候true会转换为数字1,false会转换为数字0。这样的话,在做一些多重条件判断的时候就会用到了。例如,我们想有且仅有一个条件为true时达成条件。

var a = true;
var b = false;
function onlyOneTrue(a,b,c) {
    return !!((a && !b && !c) ||(!a && b && !c) );
}
onlyOneTrue(a, b, b) ; //true
onlyOneTrue(a, b, a);  false

可以看到,在条件不多的时候,我们还可以写出来,如果需要传入5个,6个,N个参数的话,就很难处理了,在这样的业务场景下就可以将布尔值转换为数字。

var a = true;
var b = false;
function onlyOneTrue() {
    var sum = 0;
    for ( var i = 0; i < arguments.length; i++ ) {
        sum += Number(!!arguments[i])
    }
    return sum === 1;
}
onlyOneTrue(a,b,b,b,b,a,a,,b)

这样的情况下,无论我们传入多少参数,或者需要满足几个ture,只需要修改sum的条件判断就都可以了。

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

其他类型值隐式强制转换为布尔值,这种业务场景我们就见的非常多了,例如:

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

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

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

4.? : 三元运算符条件判断表达式

5.逻辑与(&&)运算符和逻辑或(||)左边的操作数

宽松相等( == )和严格相等( === )

关于这两种比较相等在社区中争论已久,有的开发者认为使用宽松相等( == )就可以了,有的开发者认为必须使用严格相等( === )。其实大可不必争论到底使用哪种方法是对的,只要我们理解了这两种相等的区别,就会恍然大悟。

两者区别: 宽松相等( == )允许在相等比较中进行强制类型转换,而严格相等( === )是不允许在相等比较中进行强制类型转换的。

虽然==比===做的事情更多,工作量大一些,在强制类型转换的时候需要多花费一点时间,但是仅仅是微秒(百万分之一秒)的差别而已,所以这一点时间并不能影响我们代码的性能。而在其他方面两者之间并没有什么不同。所以我们在写项目中可以放心的使用宽松相等( == ),而不是设置规范必须去使用严格相等( === )。

宽松不相等 != 就是==的相反方法,!==同理。

宽松相等

1.null 和 undefined两值之间的相等比较

var a = null;
var b;

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

a == false; //false
b == fasle; //false
a == 0; //fasle
b == 0; //fasle
a == ''; //fasle
b == ''; //fasle

由以上例子可以看出来在宽松比较( == )的时候,null和undefined是一回事的,可以进行隐式强制类型转换,但是除此之外其他值都不和他们两个相等。

所以在处理一些判断的时候,可以通过这种方式将null和undefined作为等价的方式比较,例如:

if ( a == null ) {
    
}
或
if ( a == undefined ) {
    
}
等价于
if ( a == null || a == undefined ) {
    
}

这样写的话既保证了安全性,又可以保证了代码的简洁性。

2.字符串和数字之间的比较

var a = 100;
var b = '100';
a === b; //false
a == b; //true

因为===不进行强制类型转换,所以a !== b,即100和'100'不相等,而 == 进行了隐式类型转换,所以100 == '100'。

字符串和数字之间宽松相等的比较规则,可以根据EAMAScript规范11.9.3.4-5来解读一下,即:

如果两边比较的值:一方值为数字,一方值为字符串,则对字符转进行ToNumber()转换。

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

var a = '100';
var b = 100;
var c = true;
a == c; //false
b == c; //false

这是因为根据ESMA规范,如果两边比较的值,一方值为true,一方值为其他类型,那么会对布尔值进行ToNumber()转换,先将布尔值转换为数字,再次进行比较。

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

var a = 100;
var b = [100];

a == b; //true

这是因为根据ESMA规范,如果两边比较的值,一方是字符串或者数字,一方是对象,那么会使对象进行ToPrimitive()抽象操作,然后进行比较。例如上述例子,先将[42]通过ToPrimitive抽象操作返回了'100',变成了 '100' == 100,再根据字符串和数字之间比较的规则,将'100'转换成了数字100,最后两者相等。

5.极端情况

[] == ![]  //true

我们知道所有的对象都强制类型转换之后都为true,上述例子看起来是真值和假值的比较,结果应该为false,但是事实结果为true,这是为什么呢?

我们知道!会进行布尔值强制类型转换,所以![]就被转换为了fasle,变成了[] == false, 而当布尔值和其他类型进行比较的时候,会进行ToNumber(false), 所以![]就被转换为了0,变成了false == 0,那么在布尔值和数字比较的时候,会进行ToNumber()转换,所以最后[] == ![];

有不对的地方,欢迎指出,一起讨论。

参考:

1.你不知道的js中卷

2.ESMAScript5.1规范