在上一篇文章中,我们详细讲解了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); //

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

var o = {
toString: function() {
return '5';
}
}; var foo = +o; console.log(foo); //

这时候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); // 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);                      //

console.log(time === date.getTime());    // true

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

var date = new Date(2017, 1, 1);

var time = +date;

console.log(time);    //

不过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;        //

{} + [];       //

{} + {};       // 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

JavaScript系列文章:自动类型转换-续的更多相关文章

  1. JavaScript: 自动类型转换-续

    在上一篇文章中,我们详细讲解了JavaScript中的自动类型转换,由于篇幅限制,没能覆盖到所有的转换规则,这次准备详细讲解一下. 上次我们提到了对象类型参与运算时转换规则: 1). 在逻辑环境中执行 ...

  2. JavaScript系列文章:谈谈let和const

    JavaScript系列文章:谈谈let和const   最近接触到ES6的一些相关新特性,想借let和const两个命令谈谈JavaScript在变量方面的改进. 由于let和const有很多相似之 ...

  3. JavaScript系列文章:自动类型转换

    我们都知道,JavaScript是类型松散型语言,在声明一个变量时,我们是无法明确声明其类型的,变量的类型是根据其实际值来决定的,而且在运行期间,我们可以随时改变这个变量的值和类型,另外,变量在运行期 ...

  4. 【转】JavaScript系列文章:自动类型转换

    我们都知道,JavaScript是类型松散型语言,在声明一个变量时,我们是无法明确声明其类型的,变量的类型是根据其实际值来决定的,而且在运行期间,我们可以随时改变这个变量的值和类型,另外,变量在运行期 ...

  5. JavaScript系列文章:变量提升和函数提升

    第一篇文章中提到了变量的提升,所以今天就来介绍一下变量提升和函数提升.这个知识点可谓是老生常谈了,不过其中有些细节方面博主很想借此机会,好好总结一下. 今天主要介绍以下几点: 1. 变量提升 2. 函 ...

  6. JavaScript系列文章:从let和const谈起

    注册博客园账号也有好些年了,有事没事经常来逛逛,感觉博客园的同学们一直都很活跃,相比国内其他社区来讲,这里的技术氛围很浓,非常适合学习和交流,所以博主我也决定在这里驻扎了,在这里,博主希望能坚持写一些 ...

  7. JavaScript系列文章:详解正则表达式之三

    在上两篇文章中博主介绍了JavaScript中的正则常用方法和正则修饰符,今天准备聊一聊元字符和高级匹配的相关内容. 首先说说元字符,想必大家也都比较熟悉了,JS中的元字符有以下几种: / \ | . ...

  8. JavaScript系列文章:详解正则表达式之二

    在上一篇文章中我们讲了正则表达式的基本用法,接下来博主想聊聊其中的细节,今天就从正则修饰符开始吧. 正则修饰符又称为正则标记(flags),它会对正则的匹配规则做限定,进而影响匹配的最终结果.在上次的 ...

  9. JavaScript系列文章:详解正则表达式之一

    正则表达式是一个精巧的利器,经常用来在字符串中查找和替换,JavaScript语言参照Perl,也提供了正则表达式相关模块,开发当中非常实用,在一些类库或是框架中,比如jQuery,就存在大量的正则表 ...

随机推荐

  1. 基于JQuery的获取鼠标进入和离开容器方向的实现

    做动画时,需要判断鼠标进入和退出容器的方向.网上找到的基于JQuery的实现方法,用函数封装了一下,写了一个示例.注意绑定鼠标事件用的是on(),所以JQuery版本需高于1.7. <!DOCT ...

  2. Nuclear开始

    为什么Nuclear 这里列举Nuclear在竞品中的优势: 借助浏览器本身的机制,无任何代码约定和入侵 放心使用HTML+CSS+JS observejs替代EventLoop.requestAni ...

  3. 做linux运维工程师,必须要掌握以下几个工具

    linux系统如果是学习可以选用redhat或centos,特别是centos在企业中用得最多,当然还会有其它版本的,但学习者还是以这2个版本学习就行,因为这两个版本都是兄弟,没区别的,有空可以再研究 ...

  4. asp.net将图片转成二进制存入数据库

    一.代码如下 int code = int.Parse(this.TextBox1.Text);//图片编码 string value = this.FileUpload1.PostedFile.Fi ...

  5. MapFile生成WMS

    MAP  NAME "HBWMS"  STATUS ON  SIZE 800 600  EXTENT 107.795 28.559 116.977 33.627  UNITS ME ...

  6. 传统软件和SaaS,差异究竟在哪里

    这篇文章从创业起步阶段.产品形态和产品策略.市场竞争格局三个方面比较了中美 SaaS 领域的异同,在文章的最后,作者根据自己在 Box 的工作经历对在国内做 SaaS 的公司提出了四点建议. 我曾有幸 ...

  7. Dynamics CRM 2015-超大Solution导入问题

    我们在将比较大的solution导入CRM的时候,经常会遇到超时的问题,这是因为CRM的本身的优化限制导致的,那么如何解决呢? 官方已经有了解决方案了. 在浏览完两种解决方法之后,我们要知道的是: 1 ...

  8. [swift]NSURLSession

    一.简单说明 在iOS9.0之后,以前使用的NSURLConnection过期,苹果推荐使用NSURLSession来替换NSURLConnection完成网路请求相关操作. NSURLSession ...

  9. MP3文件信息批量更改器

    以前(估计是2003年)编写一个MP3文件信息批量更改器MP3TagChanger,现放上来参考.(VB6编码) 使用方法很简单,会Winamp或者千千静听的就懂使用. http://pan.baid ...

  10. 《Ansible权威指南》笔记(3)——Ad-Hoc命令集,常用模块

    五.Ad-Hoc命令集1.Ad-Hoc命令集通过/usr/bin/ansible命令实现:ansible <host-pattern> [options]    -v,--verbose  ...