【面试官来了】浅析 valueOf & toString & toPrimitive

1,281 阅读5分钟

我正在参与掘金创作者训练营第6期,点击了解活动详情

前言

来了,来了,这”讨厌的面试官“。只见他自认帅气的摸了摸他的地中海发型,心里犯嘀咕:上次面试的那个家伙说的 webpack5(前景回顾),居然没难到他,这次看我这家伙身上找回面子。再次张开了他那”性感的厚嘴唇”说到:“准备好了吗?我们来聊聊valueOf & toString & toPrimitive吧。“

基本概念

valueOf: 返回对象的原始值

toString: 返回对象的字符串

toPrimitive: 一个内置的 Symbol 值,它是作为对象的函数值属性存在的,如果对象中存在个属性时,当一个对象转换为对应的原始值时,会优先调用此函数

细说规则

valueOf

对于基础类型的数据,直接返回该类型

boolean null undefined string number symbol

对于非原始值的重写规则如下

对象valueOf的返回值
Array数组本身
Boolean布尔值
Date返回毫秒形式的时间戳
Function函数本身
Number数字值
Object对象本身
String字符串值

toString转换规则

对象toString的返回值
Array以逗号分割的字符串,如[1,2]的toString返回值为"1,2"
BooleanTrue
Date可读的时间字符串,如"Tue Oct 15 2019 12:20:56 GMT+0800 (中国标准时间)"
Function声明函数的JS源代码字符串
Number"数字值"
Object"[object Object]"
String"字符串"

这里附带一个小知识点 toString 与 String 的异同?

相同点:都是将一个值转换成字符串

区别:

  1. toString可以传参,表示以多少位的格式输出结果;String方法传参无效
  2. null和undefined不能调用toString,而String可以转换'null'和'undefined'

toPrimitive转换规则

[Symbol.toPrimitive](hint){
  console.log(hint);
}

hint 并不需要我们手动传入,而是JS根据上下文自动判断。hint 参数的只能 string,number,default三者之一。

"string"

对象到字符串的转换,当我们对期望一个字符串的对象执行操作时,如 “alert”

"number"

// 显式转换 
let num = Number(obj); 

// 数学运算(除了二元加法) 
let n = +obj; 

// 一元减法
let delta = date1 - date2; 

// 小于/大于的比较 
let greater = user1 > user2;

// 乘法
let multiplication = user1 * 2;

"default"

在少数情况下发生,当运算符“不确定”期望值的类型时。 例如,二元加法 + 可用于字符串(连接),也可以用于数字(相加),所以字符串和数字这两种类型都可以。因此,当二元加法得到对象类型的参数时,它将依据 "default" hint 来对其进行转换。 此外,如果对象被用于与字符串、数字或 symbol 进行 == 比较,这时到底应该进行哪种转换也不是很明确,因此使用 "default" hint。

举些例子

valueOf 、 toString 、 toPrimitive 同时存在

 let user = {
    name: '面试官',
    money: 1000,
    toString() {
      return this.name
    },
    valueOf() {
      return this.money
    },
    [Symbol.toPrimitive](hint) {
      return hint == 'string' ? this.name : this.money
    }
  }
  console.log(`${user}`) // hint=string 面试官
  console.log(+user) // hint=number 1000
  console.log(user + 10) // hint=default 1010

valueOf 、 toString 同时存在

 let user = {
    name: '面试官',
    money: 1000,
    toString() {
      return this.name
    },
    valueOf() {
      return this.money
    }
  }
  console.log(`${user}`) // 面试官
  console.log(+user) // 1000

valueOf 、 toString 任意一个返回非原始值

 let user = {
    name: '面试官',
    money: 1000,
    toString() {
      return {}
    },
    valueOf() {
      return this.money
    }
  }
  console.log(`${user}`) // '1000'
  console.log(+user) // 1000

只有 valueOf

  const user = Object.create(null)
  user.name = 'John'
  user.money = 1000
  user.valueOf = function () {
    return user.money
  }
  console.log(`${user}`) // '1000'
  console.log(+user) // 1000

这个地方要停下来说一下,为什么我们要用Object.create的方式创建user对象? 因为如果是用字面量的方式声明user,虽然没有显示声明 toString() 方法,但其原型链上存在 toString() 方法,此次这里是为了创建一个纯对象,排除原型链的干扰。

下个结论

在JavaScript的运算过程中,必须是对原始值进行运算,再得到运算结果,比如 obj1+obj2 不能是一个新的对象,因此这里就涉及到当对象参与运算时,得先有一个转换过程!

对于对象类型的数据进行转换时

  1. 如果定义了 Symbol.toPrimitive 方法,则优先调用;
  2. 如果 hint 为 string 时,则优先调用toString()方法,如没有或返回的是非原始值再调用valueOf()方法;
  3. 如果 hint 为 number或default 时,则优先调用valueOf()方法,如没有或返回的是非原始值再调用toString()方法;

以上结论都是建立在该对象具有 toString()、valueOf()、Symbol.toPrimitive 三者方法任意一种存在。

另外:

  1. Symbol.toPrimitive 不能返回非原始值,否则报错;
  2. 如果是 Object.create(null) 定义的对象,尝试转换,那么会直接抛错;
  3. 万能转换方法:如果要自定义转换结果,其实只需要定义一个 toString 方法即可;

实战练习

参数累加

期望有一个函数 curry(1, 2)(3, 4, 5)(6)(7, 8) ,执行后得到累加的效果。

function curry(...agrs1) {
  let arr = [];
  const sum = function sum(...args2) {
    arr = [...agrs1, ...args2];
    return sum;
  };
  sum.__proto__[Symbol.toPrimitive] = function (hint) {
    return arr.reduce((t, c) => (t += c), 0);
  };
  return sum;
}
const res = curry(1, 2)(3, 4, 5)(6)(7, 8);
console.log(+res);

实现 a== 1&&a==2&&a==3 为 true 与 a===1&&a===2&&a===3 为 true

看到这个题目是不是脑瓜子嗡嗡的?其实如果理解到了上面说到的知识点,也不是没有思路

首先 对于 == 会进行隐式转换

class A {
  constructor(v) {
    this.a = v
  }
  valueOf() {
    return ++this.a
  }
}
var a = new A(0)
console.log(a == 1 && a == 2 && a == 3) // == 会进行隐式转换,hint为default

=== 在严格模式下不会进行隐式转换,因此在这里我们需要进行一些特殊处理

var value = 1;
Object.defineProperty(window, 'a', {
    get() {
        return value++
    }
})
console.log(a === 1 && a === 2 && a === 3)

最后

完了,完了。”这家伙怎么也答得上来?我还能保持我大佬的形象吗?“ 我们”讨厌的面试官“心想。故作镇定的说道:”回答得七七八八吧,但有瑕疵哈,有瑕疵,让底下观众告诉你吧!来呀,准备下一场面试“

各位吃瓜群众,欢迎指正哈!让这”面试官“知道山外有山!!