上一篇文章中,我们详细讲解了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: 自动类型转换

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

  3. 慎用javascript自动类型转换

    1.如果把非空对象用在逻辑运算环境中,则对象被转换为true.此时的对象包括所有类型的对象,即使是值为false的包装对象也被转换为true. 2.如果把对象用在数值运算环境中,则对象会被自动转换为数 ...

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

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

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

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

  6. JavaScript中判断变量类型最简洁的实现方法以及自动类型转换(#################################)

    这篇文章主要介绍了JavaScript中判断整字类型最简洁的实现方法,本文给出多个判断整数的方法,最后总结出一个最短.最简洁的实现方法,需要的朋友可以参考下 我们知道JavaScript提供了type ...

  7. javascript之类型转换

    JavaScript是一种无类型语言,但同时JavaScript提供了一种灵活的自动类型转换的处理方式.基本规则是,如果某个类型的值用于需要其他类型的值的环境中,JavaScript就自动将这个值转换 ...

  8. JavaScript自动生成博文目录导航

    转载于:JavaScript自动生成博文目录导航 我们在写博客的时候,如果博文里面有目录,会给人结构清晰.一种一目了然的感觉,看目录就知道这篇博文要讲解的内容,并且点击目录标题就可以跳转到 具体的内容 ...

  9. struts基于ognl的自动类型转换需要注意的地方

    好吧,坎坷的过程我就不说了,直接上结论: 在struts2中使用基于ognl的自动类型转换时,Action中的对象属性必须同时添加get/set方法. 例如: 客户端表单: <s:form ac ...

随机推荐

  1. Sequelize模糊查询

    const Sequelize = require('sequelize'); const Op = Sequelize.Op; User.findAll({ raw: true, order: [ ...

  2. 泡泡一分钟:Context-Aware Modelling for Augmented Reality Display Behaviour

    张宁 Context-Aware Modelling for Augmented Reality Display Behaviour链接:https://pan.baidu.com/s/1RpX6kt ...

  3. Dubbo的设计结构和工作原理

    (1)设计结构 Provider:暴露服务方称之为“服务提供者”. Consumer:调用远程服务方称之为“服务消费者”. Registry:服务注册与发现中心的目录服务称之为“服务注册中心”. Mo ...

  4. IPv4分类

    IPv4地址按逻辑层次分为五类 A类 保留给政府机构 A类地址第1字节为网络地址,其它3个字节为主机地址.它的第1个字节的第一位固定为0. A类地址网络号范围:1.0.0.0 - 126.0.0.0 ...

  5. linux编译Qt+mysql驱动+可执行文件移植目标机

    前言: 如果希望自己的Qt/C++程序在目标机上运行,最简单的方法就是在目标机上安装一个Qtcreater[Qtxxx.run],然后编译release的可执行文件,直接拉起即可. 但是有些环境情况比 ...

  6. Linux下安装.NET Core

    环境 { "操作系统":"CentOS 7.5 64位", "CPU":"1核", "内存":&qu ...

  7. 【Linux基础】vim如何显示文件名称

    前言 使用vim的时候有时候需要查看文件路径或者名称,本文对此进行记录. 操作过程 一般模式下 method1: :f method2: 快捷键CTRL+g/G(大小写均可); method3: 查看 ...

  8. 第1部分 Elasticsearch基础

    一.安装 es端口:9200 kibana端口:5601 brew install elasticsearch brew install elasticsearch brew services sta ...

  9. 初识IO流

    输入输出流,用来进行设备之间的数据传输. 是我们IO传输的数据是以文件的形式体现的,所以Java给我们提供了一个类,Flie用来描文件和目录 File(File parent, String chil ...

  10. redis相关文章

    redis主从复制相关文章    <redis如何实现主从数据的同步>      <一篇文章让你明白Redis主从同步>      <redis-sentinel的理解实 ...