从我们知道的开始

首先我们知道Object类型的转换和两个函数toString()和valueOf()有关,那为什么有关呢?在回答这个问题前我们先想为什么我们要进行类型转换呢?当然是因为标准是这样说的,所以我们追根溯源,从标准中找到蛛丝马迹。

为什么要转换?

为了更容易讲清楚,我选择的是Standard ECMA-262 5.1 Edition,我们先从一个双操作数的加号操作符的标准说起。
从标准中我们看到,在进行相加操作前js对值做了ToPrimitive()的内置操作,于是我们继续查找ToPrimitive()的标准
从标准中我们看到,当对Object进行ToPrimitive()操作的时候,是调用[[DefaultValue]]的内置方法,然后会传入一个hint作为PreferredType,所以我们继续查找[[DefaultValue]]的标准
关键信息如下:
如果传入的hint是String,先判断toString能否调用,再判断toString()的结果,是基本类型才返回,再判断valueOf能否调用,再判断valueOf()的结果,是基本类型才返回,否则报错。如果传入的hint是Number(或者没有hint,默认是Number),先判断valueOf,再判断toString对于普通Object,默认用hint为Number的方式来转换,对于Date类型的Object,用hint为String的方式来转换
我们再看一个双操作数的减号操作
我们看到做的是ToNumber()的内置操作,和双操作数的加号不同,我们再看ToNumber()的标准
而对于Object来说,无非是先ToPrimary()再ToNumber()
当然还有其他的一些转化方式,比如ToString()
注意Object.prototype.toString()有自己单独定义的算法,和ToString()不是同一个方法
vara={};a.toString=()=>100;a.toString();// 100; String(a);// '100'
当然还有ToBoolean()
简单粗暴

哪些需要转换,做怎样的转换呢?

显式转换
String()对应 ToString()Number()对应 ToNumber()Boolean()对应 ToBoolean()
隐式转换,大多出现在操作符运算中
我们发现不同的操作符有不同的转换规则,所以笔者这里给大家分个类,这里我们列举常见的操作符,暂时不考虑位操作符,看完整的描述,可以从标准的这里开始看

计算类的操作符转化比较简单

一元操作符
二元操作符

比较类的操作符算法会复杂一些

关系操作符
下图是我截取的比较算法的转换步骤
关键信息如下:
关系操作符都遵从这个算法,会先ToPrimitive(),hint为Number这里指定了hint为Number,所以Date类型的对象也会遵从hint Number的转化方式,别忘了不指定的话Date类型默认遵从hint String的转化方式然后当双方不都是String类型时,再将双方ToPrimitive()的结果ToNumber(),完整算法在这里
等性操作符
下图是我截取的双等号算法的对于Object类型的关键步骤
关键信息如下:
双等号比较的时候,如果双方类型相同,和三等号的算法一样如果双方类型不同且有一个是Object,而另一个不是,将Object进行ToPrimitive()操作有人问Boolean类型怎么没了,Boolean类型在算法前面分支已经转化为number类型了,完整算法在这里三等号比较就不说了,也不存在类型转化

一些代码Demo

// 普通的Object vara={};// 普通类型的ToPrimitive会遵从hint Number的转换规则 a.toString=()=>100;a.valueOf=()=>'10';a+2;// '102' -> 相当于 '10' + 2 为'102' a+'2';// '102' a>3;// true -> 进行ToPrimitive() hint为Number操作之后 -> 比较 '10' > 3 -> 不都是String类型,对'10'进行ToNumber(), 10 > 3为true a>'3';// false -> 实际比较的是 '10' > '3' 第一个字符的ascii码小,直接为false a==100;// false -> 相当于 '10' == 100为false // Date类型的Object varb=newDate();// Date类型的ToPrimitive会遵从hint String的转换规则 b.toString=()=>100;// 这里的搞怪为了测试 b.valueOf=()=>'10';b+2;// 102 -> 加号是ToPrimitive(), 100 + 2 为102 b+'2';// '1002' -> 相当于 100 + '2'为'1002' b>3;// true -> 进行ToPrimitive() hint为Number操作之后 -> 比较 '10' > 3 -> 不都是String类型,对'10'进行ToNumber(), 10 > 3为true b>'3';// false -> 实际比较的是 '10' > '3' 第一个字符的ascii码小,直接为false b==100;// true -> 进行ToPrimitive()操作 // 有些你们神奇的测试姿势 // 注意这里是单行命令行测试 {}+1;// 1 // 有人会认为答案应该是'[object Object]1',因为对Object进行ToPrimary(),先看valueOf(),发现是自身,不是基本类型,再看toString(),发现是'[object Object]',返回这个值,然后再相加 // 但是问题出在编译器会认为前面的{}是一个代码块,后面是一元操作符加号和1,所以结果为1 {}+'1';// 1 // 没错,这里证明了这个加号是一元的,将'1'转化为了number vara={};a+1;// '[object Object]1' // 老铁,这次就对了 [1,2]+1;// '1,21' // [1,2],先对Object进行ToPrimary(),先看valueOf(),发现是自身,不是基本类型,再看toString,Array的toString()相当于join(','),所以得到'1,2'再和1相加得到'1,21'

我能自定义这些转换规则吗?

es6中可以给对象设置一个属性,key是一个内置Symbol,value是个函数,函数中可以制定ToPrimary()的转换规则,key是Symbol.toPrimitive,也可以参考阮老师的es6入门-Symbol
letobj={[Symbol.toPrimitive](hint){switch(hint){case'number':return123;case'string':return'str';case'default':return'default';default:thrownewError();}}};// 上方的三种hint就是我们在标准文档中看到的三种hint,根据hint的不同可以自定义return的值 2*obj// 246 -> 乘号对应ToNumber(),先ToPrimitive(input argument, hint Number),再将基本值ToNumber() 3+obj// '3default' -> 双操作符的加号对应ToPrimitive(),没有hint obj=='default'// true -> 双等号对应ToPrimitive(),没有hint String(obj)// 'str' -> String对应ToString(),先ToPrimitive(input argument, hint String),再将基本值ToString()

一道小习题

// 实现一个add方法 functionadd(){// ... }// 满足以下类型的调用和计算 add(1)(2)(3)+1;// 7 varaddTwo=add(2);addTwo+5;// 7 addTwo(3)+5;// 10 add(4)(5)+add(2);// 11
以下是答案,请小心拉取
functionadd(n){varfn=function(x){returnadd(n+x);};fn.valueOf=function(){returnn;};returnfn;}

总结

不管是显式转换还是隐式转换,都无非是做内置的ToX()的方法,而像ToNumber()和ToString()都是基于ToPrimitive()再做转换的。ToPrimitive()调用了[[DefaultValue]](hint)方法,通过hint的不同,决定是先判断toString()方法还是valueOf()方法。注意toString()是原型上的方法,而ToString()是内置方法,是不同的!不同的操作符定义了不同的转换方法,还有一些特定的算法值得大家去细细品味。es6可以通过给对象增加Symbol.toPrimitive属性(属性值是个函数)来自定义转换规则。

参考

文中若有技术性错误或者还有可以扩展的地方,欢迎大家指出并交流
本文转载请注明出处,觉得有收获就给个赞呗~