JavaScript: 自动类型转换-续

上一篇文章中,我们详细讲解了JavaScript中的自动类型转换,由于篇幅限制,没能覆盖到所有的转换规则,这次准备详细讲解一下。

上次我们提到了对象类型参与运算时转换规则:

1). 在逻辑环境中执行时,会被转换为true

2). 在字符串环境和数字环境中,它的valueOf()方法和toString()方法会依次被调用,然后根据返回值进行再次转换。首先,valueOf()方法会被调用,如果其返回值是基础类型,则将这个返回值转为目标类型,如果返回值不是基础类型,则再试图调用toString()方法,然后将返回值转型。如果最终的返回值不是基础类型,则转型会抛出一个异常,如果是基础类型,则会相应的转为字符串或数字。

接着上次的讲,当加号“+”作为一元操作符应用在对象类型上面时,valueOf()和toString()方法,将会有机会被调用,最终返回值会被转为数字类型,我们因而会得到一个数字或NaN。先来看看valueOf()和toString()的调用顺序:

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

var foo = +o;

console.log(foo);    // 3

可以看到,valueOf()方法被调用,返回了字符串类型的'3',然后被转为数字类型的3,而toString()方法并没有被调用,我们再次移除valueOf()方法:

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

var foo = +o;

console.log(foo);    // 5

这时候toString()方法就被调用了,根据其返回值'5',对象被成功转为了数字5。

估计很多初学者都会觉得,如果定义了valueOf()方法,就去调用valueOf()方法,如果没定义,就去调用toString()方法,其实是不准确的。

实际上,valueOf()方法总会在第一时间被调用,至于toString()方法的调用与否,那得看valueOf()方法的返回值了,我们上面也提到了,如果其返回值是基础类型,那么toString()方法根本没有机会被调用,而如果其返回值是引用类型,则会再试图调用toString()方法得到最终值。

通常,对象原型中的valueOf()方法返回其自身引用,拿上面的例子来讲:

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

console.log(o.valueOf() === o);  // true

我们用了全等(===)操作符来比较其valueOf()返回值和其自身,发现是完全相同的,证明对象原型中的valueOf()的返回值的确是其自身,上面结果等同于下面这段代码:

// 重写实例中的valueOf()方法,其返回值是对象自身

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

console.log(o.valueOf() === o);  // true

现在我们稍加修改,就可以看出在类型转换过程中,到底发生了什么:

var o = {};

Object.prototype.valueOf = function() {
    
    console.log('valueOf() called');

    return [];
};

Object.prototype.toString = function() {
    
    console.log('toString() called')
        
    return '5';
}

var a = +o;

// output: valueOf() called
// output: toString() called

console.log(a);        // 5

var b = o + '';

// output: valueOf() called
// output: toString() called

console.log(b);        // '5'

上面的代码中,我们改为修改原型方法valueOf()和toString(),分别在方法内部添加了控制台输出语句,另外,在valueOf()内部我们返回了一个数组对象。在对象参与运算时可以看到,两个方法依次被调用,不管是数字环境还是字符串环境,都先调用了valueOf()方法,由于返回值不是基础类型,所以还需再调用toString()方法,得到一个最终的返回值,然后将其转为目标类型。如果我们将valueOf()中的数组返回值替换为一个基础类型,toString()方法将不会有机会执行,大家可以亲自试一下。

上面也提到,对象原型的valueOf()方法默认是返回对象自身的,实际上,常见对象类型的valueOf()方法都会返回其自身:

var o = {};

var fn = function(){};

var ary = [];

var regex = /./;


o.valueOf() === o;                // true

fn.valueOf() === fn;              // true

ary.valueOf() === ary;            // true

regex.valueOf() === regex;        // true

不过有个特殊的例外,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()会被先行调用。下面这段话是ECMA规范中关于Date类型转为基础类型的描述

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

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

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

var foo = +[];            // 0 ( [] -> '' -> 0 )

var foo = +[3];           // 3 ( [3] -> '3' -> 3 )

var foo = +[3, 5];        // NaN ( [3, 5] -> '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

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

对象的类型转换规则,就先讲到这里,下面来讲一下比较操作符中的类型转换

比较操作符有以下几种:>, >=, <, <=, ==, ===。除了最后的全等操作符以外,其他几个在比较不同类型的数据时,均存在值的类型转换。

对于前四种来说,都遵循着以下规则:

1). 当两个操作数都为字符串类型时,不进行数据类型转换,直接比较每个字符

2). 当两个操作数不同时为字符串时,将操作数转为数字类型,然后进行比较

3). 如果操作数中存在对象类型,先将对象转为基础类型,然后再根据上面两条进行值的比较。

而对于“==”操作符,则是多了一条特殊的规则:null和undefined在比较时不进行数据转换,null和自身比较、null和undefined比较都会返回true,和其他值比较都会返回false;undefined和自身比较、undefined和null比较都会返回true,和其他值比较都会返回false。

以下比较操作不存在数据类型转换:

'3a' < '3b';          // true

'' == '0';            // false

null == undefined;    // true

null == 0;            // false

undefined == false;   // false

需要注意的是最后两个个表达式,由于我们在上一篇文章中讲到,null值在数字环境下会转型为0,很多人觉得这个表达式结果为true,但是不要忽略了上面关于null和undefined的规则,这里是不会有类型转换发生的,同样的,undefined在比较操作符中也只会识别其自身和null值,并且不会发生数据类型转换。

在下面几个表达式中,操作数不全为字符串,所以要将操作数转为数字后再进行比较:

3 == '3';             // true

3 < '5';             // true

0 == '0';        // true

0 == '';        // true

0 == false;        // true

1 <= true;            // true

null >= 0;            // true

注意,最后一个表达式中的null,在遇到>、>=、<、<=这几个操作符时会被转为数字0的,这与上面的规则有所不同。

最后,对象在参与逻辑运算时,同样会遵循前面的转型规则:

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

o > 4;        // true

特别注意的是,前面我们介绍到,对象在条件语句中是视为true的,但要避免下面这样的比较:

if ([]) {
    // todo
}

if ([] == true) {
    // todo
}

第二个条件语句块是不会执行的,原因在于空的数组对象被转为数字0了,而true被转为数字1,比较结果为false,所以里面的代码永远无法得到执行,开发时要警惕这样的写法。

写了这么多关于自动类型转换的内容,大家也可以体会到JS有多么的灵活,想要驾驭好这门语言,不是件容易的事,还需细细体会,好好研究才行。

本文完。

 

参考资料:

http://es5.github.io/#x11.8.5

http://es5.github.io/#x11.9.3

http://www.2ality.com/2012/01/object-plus-object.html

http://www.2ality.com/2013/04/quirk-implicit-conversion.html

https://www.united-coders.com/matthias-reuter/all-about-types-part-2/

http://www.adequatelygood.com/Object-to-Primitive-Conversions-in-JavaScript.html

posted @ 2016-11-03 08:56  liuhe688  阅读(1126)  评论(0编辑  收藏  举报