renaesop/blog

从JS中的valueOf谈开

Opened this issue · 0 comments

ValueOf是JavaScript中Object原型上少数几个方法之一,应该不能算是很偏门的函数,但是只有《JavaScript权威指南》上面有只言片语的描述,而对ES2015中相应的Symbol.toPrimitive更是鲜有提及。

首先,给出一个结论,valueOftoString几乎都是在出现操作符(+-*/==><)时被调用, 并且valueOf几乎没有什么用。

那么,valueOf的作用是什么呢?按照语言标准上的说法就是,用于toPrimitive需要Number时,而toPrimitive出现的时机,说得简单一点就是,当需要一个Number或者String,但是被传入了一个对象时,就会执行这个操作。有一个常见的用法实际上是使用了valueOf:

将Date对象转换为时间戳时,会很自然的用+ new Date(), 实际上是悄悄地调用了new Date().valueOf()

详细地说,有如下场景,会出现偏好结果是Number的toPrimitive,也就是说valueOf可能被调用(如果没有valueOf的话,可以被toString等替代):

  • ToNumber

具体而言,当obj前后操作符是加法,以及减法、乘法、除法,以及调用Number(obj)以及new Number(obj)时。值得注意的是,parseInt, parseFloat等方法实际上不会调用ToNumber, 他们调用的是偏好String的toPrimitive。举个代码的例子:

class Test {
  valueOf() {
    return 1;
  }
  toString() {
    return '2';
  }
}

const a = new Test();

console.log(parseInt(a, 10)); //  打印出2,也就是toString被调用了
console.log(Number(a));//  打印出1,也就是valueOf被调用了

const obj = {};
obj[a] = 1;
console.log(obj); // 打印出 {‘2’: 1},也就是toString被调用了

const b = 0;
const c = '0';
console.log(a + b); // 打印出 1, 也就是valueOf被调用了
console.log(a + c);// 打印出 10, 也就是还是ValueOf被调用
  • 比较大小

直接上代码:

class Test {
  constructor(val) {
    this.__val = val;
  }
  valueOf() {
    return this.__val;
  }
}

const a = new Test('12');
const b = new Test(2);
console.log(a < b); // 打印出false,说明valueOf被调用,且两者不都是string时,转换为Number
                                // 且如果不能转为Number,则值为NaN

嗯,我们可以得出如下结论,在应该使用“值”,也就是数值的地方,如果出现了对象,就会调用valueOf。

再给出一个例子,证明valueOf不存在时,可以用toString作为替代品

class Test {
  toString() {
    return '2';
  }
}

const a = new Test();

console.log(parseInt(a, 10)); //  打印出2,也就是toString被调用了
console.log(Number(a));//  打印出2,也就是toString被调用了

const obj = {};
obj[a] = 1;
console.log(obj); // 打印出 {‘2’: 1},也就是toString被调用了

const b = 0;
const c = '0';
console.log(a + b); // 打印出 1, 也就是toString被调用了
console.log(a + c);// 打印出 10, 也就是还是toString被调用

但是反过来是不成立的,有的地方必须使用toString, 比如说把对象作为对象的key使用时,以及向parseInt中传入对象时。

到这里,可以得出一个结论,一个对象想要在希望转化为数字的地方,通过给出特殊的valueOf来给出不同于期望转化为字符串的地方的值,最好的例子就是Date对象。

另外,我们频繁提到的toPrimitive这个操作,在ES2015标准中,已经真的添加了这个方法,并且这个方法会比toStringvalueOf的优先级都高,并且嘛,几乎都可以替代这俩货了, 给个例子:

class Test {
  valueOf() {
    return 1;
  }
  toString() {
    return '2';
  }
  [Symbol.toPrimitive](hint) {
    console.log(hint);
    return 3;
  }
}

let a = new Test();

const obj = {};
obj[a] = 1;
console.log(obj); // => string  { '3': 1 }

const b = 0;
const c= '0';
console.log(a + b); // => default 3, default相当于number
console.log(a + c); // => default 30

console.log(parseInt(a, 10)); // => string 3
console.log(Number(a)); // => number 3

上述代码中Symbol.toPrimitive方法可以接收参数,表示期望的类型,就像我们提到的,如果是string,就是期望获得字符串(也就是之前说的调用toString),如果是default或者number则希望获取一个数字(也就是之前说的优先调用valueOf,否则调用toString)。

可以看出,Symbol.toPrimitive是完完全全可以取代掉valueOf,甚至toString

另外,++运算符也可以触发偏好Number的toPrimitive,而且很有意思的是toPrimitive系是内建函数,可以返回左值,所以可以用到对象上:

class Test {
  constructor(val) {
    this.__val = val;
  }
  [Symbol.toPrimitive](hint) {
    return this.__val;
  }
}

let a = new Test(1);
console.log(++a); // => 2

let b = new Test('2');
b++;
console.log(b); // => 3

let c = '1';
console.log(typeof c) // => 'string'
console.log(++c)
console.log(typeof c) // => 'numer', ++ 确实有转型的作用