JavaScript系列文章:自动类型转换-续
Posted js-rocker
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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://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
以上是关于JavaScript系列文章:自动类型转换-续的主要内容,如果未能解决你的问题,请参考以下文章
Javascript数组系列四之数组的转换与排序Sort方法
也谈SQL Server 2008 处理隐式数据类型转换在运行计划中的增强 (续)
也谈SQL Server 2008 处理隐式数据类型转换在运行计划中的增强 (续)