深入理解Javascript中Object类型的转换

深入理解Javascript中Object类型的转换

从我们知道的开始

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

为什么要转换?

为了更容易讲清楚,我选择的是Standard ECMA-262 5.1 Edition,我们先从一个双操作数的加号操作符的标准说起。

The Addition operator ( + )

  • 从标准中我们看到,在进行相加操作前js对值做了ToPrimitive()的内置操作,于是我们继续查找ToPrimitive()的标准

ToPrimitive

  • 从标准中我们看到,当对Object进行ToPrimitive()操作的时候,是调用[[DefaultValue]]的内置方法,然后会传入一个hint作为PreferredType,所以我们继续查找[[DefaultValue]]的标准

[[DefaultValue]](hint)


关键信息如下:

  • 如果传入的hint是String,先判断toString能否调用,再判断toString()的结果,是基本类型才返回,再判断valueOf能否调用,再判断valueOf()的结果,是基本类型才返回,否则报错。
  • 如果传入的hint是Number(或者没有hint,默认是Number),先判断valueOf,再判断toString
  • 对于普通Object,默认用hint为Number的方式来转换,对于Date类型的Object,用hint为String的方式来转换


我们再看一个双操作数的减号操作

The Subtraction Operator ( - )

  • 我们看到做的是ToNumber()的内置操作,和双操作数的加号不同,我们再看ToNumber()的标准

ToNumber

  • 而对于Object来说,无非是先ToPrimary()ToNumber()


当然还有其他的一些转化方式,比如ToString()

ToString

  • 注意Object.prototype.toString()有自己单独定义的算法,和ToString()不是同一个方法
var a = {};
a.toString = () => 100;

a.toString(); // 100;
String(a); // '100'


当然还有ToBoolean()

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
var a = {}; // 普通类型的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
var b = new Date(); // 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

var a = {};
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
let obj = {
  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case 'number':
        return 123;
      case 'string':
        return 'str';
      case 'default':
        return 'default';
      default:
        throw new Error();
     }
   }
};

// 上方的三种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方法
function add() {
    // ...
}

// 满足以下类型的调用和计算
add(1)(2)(3) + 1; // 7

var addTwo = add(2);
addTwo + 5; // 7
addTwo(3) + 5; // 10
add(4)(5) + add(2); // 11


以下是答案,请小心拉取

function add(n){
  var fn = function(x) {
    return add(n + x);
  };
  
  fn.valueOf = function() {
    return n;
  };
  
  return fn;
}


总结

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

参考


文中若有技术性错误或者还有可以扩展的地方,欢迎大家指出并交流

本文转载请注明出处,觉得有收获就给个赞呗~

编辑于 2017-10-02