javascript数据隐式转换详解与分析

js是一门弱类型语言,在声明一个变量时,我们是无法明确声明其类型的,变量的类型是根据其实际值来决定的,而且在运行期间,我们可以随时改变这个变量的值和类型,另外,在变量的运行期间参与运算时,在不同的运算环境中,也会进行相应的自动类型转换。

自动类型转换一般是跟运行环境操作符联系在一起的,是一种隐式转换,其实是有一定规律性的,大体可以划分为:转换为字符串类型,转换为布尔类型,转换为数字类型

1.转换为字符串类型(to string)
当加号“+”作为二元操作符(binary)并且其中一个操作数为字符串类型时,另一个操作数将会被无条件转为字符串类型:

// 基础类型
var foo = 1 + '';          // "1"
var foo = true + '';       // "true"
var foo = undefined + '';  // "undefined"
var foo = null + '';       // "null"

// 复合类型
var foo = [1,2,3] + '';   // "1,2,3"
var foo = {} + '';        // "[object Object]"

// 重写valueOf() 和 toString()
var o = {
    valueOf: function(){
        return 3;
    },
    toString: function(){
        return 5;
    }
};

foo = o + '';    // "3"

o = {
    toString:function(){
        return 5;
    }
};

foo = o + '';   // "5"

解析:
js 无论是通过 “” + value 或者 String(value) 将value(value为原始值)值转换为字符串都遵循一个原则:
通过引擎内部的ToString()操作将原始值(基础数据类型)转换为字符串。

引擎内部的ToString()操作是一个内部操作,我们使用ES语言无法访问到。

ToString()操作的对应表如下所示:

参数 结果
undefined “undefined”
null “null”
布尔值 “true”或者”false”
数字类型 数字的字符表示
字符串 无需转换

提示:可能有些开发者喜欢用对象或者字面量的toString()方法来转换字符串,但是该对象变量是undefined或者null的话程序会报错。使用”” + value 和 String(value)则不存在undefined和null报错的问题,因为ToString()操作之中包含了null和undefined二者,而toString()方法不调用引擎内部的ToString()操作。

所以,在上面的案例之中基础类型正是遵循了ToString()操作将原始值转换为字符换然后拼接。

那么复合类型呢?
js中复合类型通过 “” + value 或者 String(value) 来转换成字符串也遵循一个原则:
先将复杂的复合值转换为简单的原始值,然后再通过引擎内部的ToString()操作将转换后的简单值转换为字符串。

那么,这里解析复杂值如何转换为简单的原始值:

  1. 如果使用的是 “” + value(当然,这里的value是一个复杂值)的方法,那么value会调用内部的ToPrimitive(Number)操作;该操作会先调用对象内部的valueOf()方法,如果调用valueOf()返回的是一个原始值那么结束操作,否则继续调用对象内部的toString()方法,如果调用toString()方法返回的是原始值,则结束操作,否则抛出类型错误”TypeError: Cannot convert object to primitive value”。
  2. 如果使用的是 String(value),操作和 “” + value 相比,String(value)先调用对象内部的toString()方法,返回的如果不是原始值再调用对象的valueOf()方法。两者只是在调用对象方法的顺序上面不同,其他一样。

关于 “” + value 和 String(value)的详细说明,可以查看将value值转换为字符串方法以及原理详解这篇博客。

现在我们回过头再看上面的例子:

var foo = [1,2,3] + ''; 

这里[1,2,3]调用内部的ToPrimitive(Number)操作,调用自身的valueOf操作,返回的还是[1,2,3],它还是一个复杂值,再调用自身的toString()方法,返回”1,2,3”,是一个字符串原始值,最后 “1,2,3”+”“=”1,2,3”得到结果。

var foo = {} + '';

这里{}调用内部的ToPrimitive(Number)操作,调用自身的valueOf操作,返回的结果是{},它还是一个复杂值,再调用自身的toString()方法,返回’[object Object]’,是一个字符串原始值,最后’[object Object]’ + ” = ‘[object Object]’;

var o = {
    valueOf: function(){
        return 3;
    },
    toString: function(){
        return 5;
    }
};

foo = o + ''; // "3"

o = {
    toString:function(){
        return 5;
    }
};

foo = o + '';   // "5"

这里的第一个定义的o对象重写的o的valueOf方法和toString方法, o + ”,调用o的ToPrimitive(Number)操作,首先调用o内重写的valueOf方法,返回值是3,它是一个数字原始值,结束操作。然后通过ToString()操作将返回的3转换为字符”3”,最后”3” + “” = “3”;

这第二个把o又重新指向一个新的对象,该对象重写了toString方法, o + ”, 调用o的ToPrimitive(Number)操作,首先调用o内重写的valueOf方法,返回值是还是o这个对象,它不是一个原始值,继续调用o的toString()方法,toString方法在这对象内部是重写的,返回值是5这个数字原始值,所以结束操作。然后通过ToString()操作将返回的5转换为字符”5”,最后”5” + “” = “5”;

这里如果对于解释的 valueOf 和 toString方法不是很理解可以查看相关博客。

2.转换为布尔类型(to boolean)

当数字在逻辑环境中执行时,会自动转为布尔类型。0和NaN会自动转换为false,其余数字都被认为是true,代码如下:

// 0和NaN为false,其余均为true
if(0){
    console.log('true');
}else{
    console.log('false');
}
// output false

if(-0){
    console.log('true');
}else{
    console.log('false');
}
// output false

if(NaN){
    console.log('true');
}else{
    console.log('false');
}
// output false

// 其余数字均为true
if(-3){
    console.log('true');
}else{
    console.log('false');
}
// output true

if(3){
    console.log('true');
}else{
    console.log('false');
}
// output true

从上面的代码中可以看出, 非0负值也会被认为是true,这一点需要注意。

字符串转为布尔类型(from string)

和数字类似,当字符串在逻辑环境中执行,也会被转为布尔类型。空字符串会被转为false,其它字符串都会转为true,代码如下:

// 空字符串为false
if(''){
    console.log('true');
}else{
    console.log('false');
}
// output false

// 其它字符串均为true
if('0'){
    console.log('true');
}else{
    console.log('false');
}
// output true

if('false'){
    console.log('true');
}else{
    console.log('false');
}
// output true

undefined和null转为布尔类型(from undefined and null)

undefined和null在逻辑环境中执行时,都被认为是false,看下面代码:

// undefined和null都为false
if(undefined){
    console.log('true');
}else{
    console.log('false');
}
//output false


if(null){
    console.log('true');
}else{
    console.log('false');
}
// output false

对象转为布尔类型(from object)

当对象在逻辑环境中执行时,只要当前引用的对象不为空,都会被认为是true。如果一个对象的引用为null,根据上面的介绍,会被转换为false。虽然使用typeof检测null为”object”,但它并不是严格意义上的对象类型,只是一个对象空引用标识

另外,我们这里的逻辑环境不包括比较操作符(==),因为它会根据valueOf()和toString()将对象转为其他类型。

看实例:

// 字面量对象
var o = {
    valueOf:function(){
        return false;
    },
    toString:function(){
        return false;
    }
};

if(o){
    console.log('true');
}else{
    console.log('false');
}
// output true

// 函数
var fn = function(){
    return false;
};

if(fn){
    console.log('true');
}else{
    console.log('false');
}
// output true


// 数组
var ary = [];
if(ary){
    console.log('true');
}else{
    console.log('false');
}


// 正则表达式
var regex = /./;
if(regex){
    console.log('true');
}else{
    console.log('false');
}

可以看到,上面的对象都被认为是true,无论内部如何定义,都不会影响最终结果。
正式由于对象总是被认为true,使用基础类型的包装类时,要特别小心:

if(new Boolean(false)){
    console.log('true');
}else{
    console.log('false');
}
// output true;


if(new Number(0)){
    console.log('true');
}else{
    console.log('false');
}
// output true


if(new Number(NaN)){
    console.log('true');
}else{
    console.log('false');
}
// output true

if(new String('')){
    console.log('true');
}else{
    console.log('false');
}
// output true

上面介绍了这么多, 还有几个例子需要提一下,那就是逻辑非,逻辑与和逻辑或操作,连用两个逻辑非可以把一个值转为布尔值,而使用逻辑与和逻辑或时,根据上面的规则,参与运算的值会被转换为相应的布尔类型:

// 下面几个转为false
var isFalse = !!0;   // false
var isFalse = !!NaN; // false
var isFalse = !!'';  // false  
var isFalse = !!undefined;  // false
var isFalse = !!null; // false

 // 下面都转为true
 var isTrue = !!3;  // true
 var isTrue = !!-3; // true
 var isTrue = !!'0';// true
 var isTrue = !!{}; // true

  // 逻辑与
  var foo = 0 && 3;  // 0
  var foo = -3 && 3; // 3

   // 逻辑或
   var foo = 0 || 3;  // 3
   var foo = -3 || 3; // -3 

3.转换为数字类型(to number)

操作数在数字环境值参与运算时,会被转为相对的数字类型值,其中的转换规则如下:
- 字符串类型转为数字(from string):空字符串被转为0,非空字符串中,符合数字规则的会被转换为对应的数字,否则视为NaN
- 布尔类型转为数字(from boolean):true转为1, false转为0
- null被转为0, undefined被转为NaN
- 对象类型转为数字(from object):valueOf()方法先试图被调用,如果调用返回的结果为基础类型,则再将其转为数字,如果返回结果不是基础类型,则会再试图调用toString()方法,最后试图将返回结果转为数字,如果这个返回结果是基础类型,则会得到一个数字或NaN,如果不是基础类型,则会抛出异常。

对象类型转为数字的原则:先想方将对象类型转换为基本类型,在将基本类型转为数字。

一个其他类型的值被转换为数字,跟其参与运算的操作符有很密切的关系,下面我们就来介绍:

加号(+)作为一元操作符(unary)时,引擎会试图将操作数转换为数字类型,如果转型失败,则会返回NaN

var foo = +'';   // 0
var foo = +'3';  // 3
var foo = +'3px';// NaN
var foo = +false;// 0
var foo = +true; // 1
var foo = +null; // 0
var foo = +undefined;// NaN 

上面代码中,对于不符合数字规则的字符串,和直接调用Number()函数效果相同,但是和parseInt()有些出入:

var foo = Number('3px'); // NaN
var foo = parseInt('3px'); // 3

可以看出,parseInt对于字符串参数比较宽容,只要起始位置符合数字类型标准,就逐个解析,知道遇见非数字字符为止,最后返回已解析的数字部分,转为数字类型。

加号(+)作为二元操作符时,我们上面也提到过,如果一个操作数为字符串,则加号”+”作为字符串连接符,如果两个操作符都不是字符串类型,则会作为加法操作符,执行加法操作,这个时候,其他数据类型也会转为数字类型:

var foo = true + 1; // 2
var foo = true + false; // 1
var foo = true + null;  // 1
var foo = null + undefined; // NaN
var foo = null + NaN; // NaN

上面加法运算过程中都出现了类型转换, true转为1,false转为0,null转为0,undefined转为NaN,最后一个例子中,null和NaN运算时,是先转为0,然后参与运算,NaN和任何其他数字类型运算都会返回NaN

对于undefined转为NaN似乎很好理解,但为什么null会转为0呢?这里也有些历史渊源,C语言中,空指针其实是设计为0值的:

// 空指针的值为0
int *p = NULL;
if(p === 0){
    printf("Null is 0");
} 
// output "NULL is 0"

编程语言的发展也是有规律的,语言之间也存在这密切的关联,新的语言总是会沿用老的传统,继而添加一些新的特性。

另外,我们可别忘了减号”-“操作符,当减号(-)作为一元操作符(unary negation)时,也会将操作数转换为数字,只不过转换的结果与上面相反,合法的数字都被转为负值(即负数)

除了加号以外的其他二元操作符,都会将操作数转为数字,字符串也不例外(如果转型失败,则返回NaN继续参与运算):

var foo = '5' - '2';   // 3
var foo = '5' * '2';   // 10
var foo = '5' / '2';   // 2.5
var foo = '5' % '2';   // 1
var foo = '5' >> '1';  // 2
var foo = '5' ** '2';  // 25

var foo = '5' * true;  // 5
var foo = '5' * null;  // 0
var foo = '5' * undefined; // NaN
var foo = '5' * NaN; // NaN

上面的操作符中,位移和求幂操作符平时用的不多,不过在某些场景下(比如算法中)还是会挺实用的。

我们都知道,JavaScript中的数字类型都已浮点存储,这就意味这我们不能像C和java那么也直接求整除结果,而是通过相关的函数进一步实现的,如果通过位移可以简化不少,而求幂操作也可以直接通过幂运算符算出结果:

// 浮点型运算
var foo = 5/2;  // 2.5

// 整除操作
var foo = Math.floor(5/2); //2

// 向右位移一位实现整除
var foo = 5 >> 1;   // 2

// 求幂函数
var foo = Math.pow(5,2); // 25

// 求幂运算
var foo = 5**2; // 25

除了上面的操作符之外,递增和递减操作符也会将操作数转为数字:

var foo = '';
++foo; // foo:1

var foo = '3';
++foo;   // foo:4

var foo = true;
++foo;   // foo:2

var foo = null;
++foo;   // foo:1

var foo = undefined;
++foo;   // foo:NaN

var foo = '3px';
++foo;  // foo:NaN

上面就是基本数据类型在数字环境下的转换规则。对于对象类型,同样有一套机制,我们上面也提到了,valueOf()方法和toString()方法会在不同的实际被调动,进而得到相应的返回值,最后根据返回值再进行类型转换,将其转为目标类型。

下面说一个特殊情况:
Date类型的valueOf()会返回一个毫秒数。

var date = new Date(2017,1,1);
var time = date.valueOf();
console.log(time);   // 1485878400000
console.log(time === date.getTime()); // true

所以我们就会很容易明白,Date实例上应用用一元加号操作符,是如何返回一个数字的:

var date = new Date(2017,1,1);
var time = +date;
console.log(time);  // 1485878400000

不过Date真是一个神奇的动物,如果我们直接跟拿它的一个时间毫秒数相加,并不会得到期望的结果:

var date = new Date(2017,1,1);
var time = date + 10000;
console.log(time); //'Wed Feb 01 2017 00:00:00 GMT+0800 (CST)10000'

它竟然转为了字符串,然后与数字进行了字符串连接操作!为什么会这样呢?原因在于,对于一元加号操作符运算,目的很明确,就是求正操作,因此引擎调用了其valueOf()方法,返回时间戳数字,而对于后者的二元加号操作运算,其存在加法和连接符这样的二义性,所以引擎可以有选择地将操作数转为不同的目标类型,与其他对象不同的是,Date类型更倾向于转为字符串类型,所以toString()会被先行调用。

对象在转为基础类型时,通常会调用toPrimitive(hint)这样的方法,传入一个提示参数,指定其目标类型,其他对象的默认值都是number,而Date类型与众不同,它的默认值是string。

上面我们也提到了,一元加号操作符是求正运算,所以引擎能够识别并为其指定number目标类型,而二元加号操作符存在二义性,引擎使用了default作为提示参数,Date类型默认值认为是string,所以我们也理解了上面的例子,即使是 Date对象和数字相加,它也不会先调用valueOf()方法得到数字,而是先调用toString()得到一个字符串。

上面讲了这么多,相信大家对于对象类型的转型都熟悉了,那么对于常见的对象,究竟是如何转为基础类型的呢?举个例子:

var foo = +[];   //0
var foo = +[3];  //3
var foo = +[3,5];//NaN

从上面的代码可以看出,对于数组对象来说,要转为数字,就要遵循对象类型的转型规则,因为数组原型的valueOf()方法会返回其自身引用,所以最终会再次试图调用其toString()方法,而它的toString()会返回一个字符串,这个字符串是由逗号分隔的数组元素集,那就很容易理解了,对于空数组,必然返回一个空字符,然后这个空字符串转型为数字之后就会变为0,而对于非空数组,如果只有一个元素并且元素可以转为数字,则结果第一个元素对应的数字,如果有多个元素,因为toString()返回的结果中存在逗号,所以无法转型成功,会返回一个NaN。

但如果我们尝试数组和一个数字相加,则还是会得到一个字符串的结果:

var foo = [] + 3;  // '3'
var foo = [3] + 3; // '33'
var foo = [3,5] + 3; // '3,53'

你也许会说,这不是很像上面的Date类型吗?是的,结果看上去很相似,但其内部的执行过程还是有差异的,它的valueOf()会先执行,出现上面的结果,但是由于valueOf()返回this,然后再次调用toString()返回了字符串,加号操作符在这里成了字符串连接符了。

类似的还有字面量对象,看下面例子:

var foo = {} + 0;   // '[object Object]0'
var foo = {} + [];  // '[object Object]'
var foo = {} + {};  // '[object Object][object Object]'

不过,如果是在命令行直接输入下面表达式,结果会所有出入:

{} + 0;   // 0
{} + [];  // 0
{} + {};  // NaN

其原因是,前面的字面量对象被解释成了代码块,没有参与运算,只有后面的一部分会返回最终的结果,后面的转换过程可以参照以上我们讲解的内容。
对象的类型转换规则,就先讲到这里。

你可能感兴趣的:(javascript)