valueOf和toString

1,674 阅读4分钟

类型分类

JS中的类型分为两大类: primitive(基本数据类型)object(复杂数据类型)

其中primitive类型的数据有7种,分别为: Null, Undefined, Number, String, Boolean, Symbol, BigInt。

不是primitive类型的值,都可以被认为是object类型

当object类型和primitive类型进行运算的时候,JS会尝试将object类型的数据转换为primitive类型的数据后再进行相应的运算

在ECMAScript文档,为object定义了一个内部方法toPrimitive,当JS需要将尝试将对象数据类型转换为基本数据类型的时候,就会调用这个方法

在ES6中,为引用数据类型提供了Symbol.toPrimitive属性,我们可以通过该属性来重写object的内部方法toPrimitive

toString

将一个对象转换为对应的字符串形式,该方法返回一个表示该对象的字符串

toString被定义在了Object方法的显示原型对象上

 const num = 123
 const bool = true
 const str = 'Klaus'
 const sym = Symbol('sym')
 ​
 console.log(typeof num.toString(), num.toString()) // => string '123'
 console.log(typeof bool.toString(), bool.toString()) // => string 'true'
 console.log(typeof str.toString(), str.toString()) // => string 'Klaus'
 console.log(typeof sym.toString(), sym.toString()) // string 'Symbol(sym)'

当对象调用 toString 方法时,默认返回的格式是 [object Type],其中:

  • 第一个 object 是固定的,表示这是一个对象。
  • 第二个 Type 是对象的内部 [[Class]] 属性的值,这通常是对象的类型描述,而不是它的构造函数。

对于一些特殊的对象,如果函数,数组等都重写了自己的toString方法

 const obj = {}
 const arr = [1, 2, 3]
 const fun = () => {}
 const err = new Error('我是错误信息')
 const date = new Date()
 ​
 console.log(typeof obj.toString(), obj.toString()) // => string [object Object]
 ​
 // 数组输出的是arr.join(',') ---- 如果是空数组,返回的就是空字符串
 console.log(typeof arr.toString(), arr.toString()) // => string 1,2,3
 ​
 // 函数返回函数体本身
 console.log(typeof fun.toString(), fun.toString()) // => string () => {}
 ​
 // 错误对象 输出message属性值
 console.log(typeof fun.toString(), fun.toString()) // => string "Error: 我是错误信息"
 ​
 // 日期对象输出字符串日期格式
 console.log(typeof date.toString(), date.toString())
 // => string Fri Nov 05 2021 13:57:12 GMT+0800 (中国标准时间)

toString是最精确的类型判断方式之一

 console.log(toString.call(''))           // => [object String]
 console.log(toString.call(22))           // => [object Number]
 console.log(toString.call(undefined))    // => [object Undefined]
 console.log(toString.call(null))         // => [object Null]
 console.log(toString.call(new Date))     // => [object Date]
 ​
 console.log(toString.call(Math))         // => [object Math]
 console.log(toString.call(globalThis))   // => [object global]
 ​
 console.log(toString.call(()=>{}))       // => [object Function]
 console.log(toString.call({}))           // => [object Object]
 console.log(toString.call([]))           // => [object Array]
 ​
 console.log(toString.call(new Set()))    // => [object Set]
 console.log(toString.call(new Map()))    // => [object Map]

Symbol.toStringTag

在对象没有重写Object.prototype.toString方法的情况下, 对象调用toString方法时,返回结果为[object Object]

对此 JS提供了一个内置的symbol值 Symbol.toStringTag,通过实现这个函数,我们可以修改对象的内部 [[Class]] 属性的值

 const user = {
   name: 'Klaus',
   age: 23,
 ​
   // Symbol.toStringTag 是一个get访问器函数
   // getter需要返回字符串类型值,如果不是字符串类型值则静默失效而不是进行隐式类型转换
   get [Symbol.toStringTag]() {
     return this.name
   }
 }
 ​
 console.log(user.toString()) // => [object Klaus]

valueOf

返回当前对象的原始值。返回当前对象所对应的基本数据类型值

如果当前对象无法获取到对应的基本数据类型,那么会将对象本身原封不动的返回

valueOf被定义在了Object方法的显示原型对象上

 // valueOf --- 如果能返回原始值,就返回原始值
 const num = 123
 const bool = true
 const str = 'Klaus'
 ​
 console.log(num.valueOf()) // => 123
 console.log(bool.valueOf()) // => true
 console.log(str.valueOf()) // => Klaus
 // valueOf --- 如果无法返回原始值,就返回参数对象本身
 const obj = {}
 const arr = [1, 2, 3]
 const fun = () => {}
 ​
 console.log(obj.valueOf()) // => {}
 console.log(arr.valueOf()) // => [ 1, 2, 3 ]
 console.log(fun.valueOf()) // => [Function: fun]
 // Date对象 重写了valueOf方法 --- 返回时间所对应的时间戳
 const date = new Date()
 console.log(date.valueOf()) // => 1636092865039

toPrimitive

在ECMAScript文档,为object定义了一个内部方法toPrimitive, 其会在进行类型转换或者运算的时候被调用。

 // toPrimitive的伪代码形式如下:
 toPrimitive(target, PreferredType = 'default': 'string' | 'number')
  • 如果没有定义PreferredType, 则默认为defaultdefault的表现行为和number完全一致 」

  • 执行流程

    • PreferredType 为 number

      • 优先调用 valueOf,若返回基本类型则使用该值。
      • 否则调用 toString
      • 还不能转换,直接报错
    • PreferredType 为 string

      • 优先调用 toString,若返回基本类型则使用该值。
      • 否则调用 valueOf
      • 还不能转换,直接报错

[Symbol.toPrimitive]

通过实现 Symbol.toPrimitive,我们可以完全控制对象到基本类型的转换。

如果实现了Symbol.toPrimitive方法,JS就不会再去调用原生的valueOftoString

 class Foo {
   constructor(num) {
     this.num = num;
   }
 ​
   // hint的值和规则和toPrimitive的参数规则完全一致
   [Symbol.toPrimitive](hint) {
     if (hint === 'number') {
       return this.num;
     }
     if (hint === 'string') {
       return String(this.num);
     }
     return null;
   }
 }
 ​
 const foo = new Foo(10);
 console.log(Number(foo)); // 10
 console.log(String(foo)); // "10"
 ​
 // 比较运算和加法运算的时候,hint的值是default,并不是number
 console.log(foo == 10) // => false == 10 -> false
 console.log(foo + 2) // => false + 2 -> 0 + 2 -> 2
 console.log(foo + '2') // => false + '2' -> 'false2'
 ​
 // 全等判断 和 转布尔值时,不会调用Symbol.toPrimitive
 console.log(foo === '10') // => false
 console.log(!!foo) // => true

某些对象会重写Object原型上的toString和valueOf方法

 const dateValueOf =  Date.prototype.valueOf
 const dateToString =  Date.prototype.toString
 ​
 // 添加Date的valueOf日志
 Date.prototype.valueOf = function () {
   console.log('date valueOf')
   return dateValueOf.call(this)
 }
 ​
 // 添加Date的toString日志
 Date.prototype.toString = function () {
   console.log('date toString')
   return dateToString.call(this)
 }
 ​
 const date = new Date()
 ​
 // Date在进行加法运算和判等运算时,优先执行toString方法
 console.log(date + 1)
 console.log(date == 2)

特殊判等

如何让 a === 1 && a === 2 && a === 3 的结果为 true

 class A {
   constructor(value) {
     this.value = value;
   }
 ​
   valueOf() {
     return this.value ++
   }
 }
 ​
 const a = new A(1)
 console.log(a == 1 && a == 2 && a == 3)  // => true

如何让 a == 1 && a == 2 && a == 3 的结果为 true

 let target = {
   a: 0
 }
 ​
 let proxy = {}
 ​
 Object.defineProperty(proxy, 'a', {
   get() {
     target.a++
     return target.a
   }
 })
 ​
 console.log(proxy.a===1 && proxy.a===2 && proxy.a===3) // => true
 let target = {
   a: 0
 }
 ​
 const proxy = new Proxy(target, {
   get(target, key, receiver) {
     if (key === 'a') {
       target[key]++
       return Reflect.get(target, key, receiver)
     }
   }
 })
 ​
 console.log(proxy.a===1 && proxy.a===2 && proxy.a===3) // => true