JS中的valueOf与toString区别简介

146 阅读4分钟

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

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

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

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

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

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

toString

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

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 Object]

对于一些特殊的对象,如果函数,数组等都重写了自己的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 () => {}

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

Symbol.toStringTag对应的值是一个get访问器函数,通过实现这个函数,我们可以自定义toString方法返回的type值

const user = {
  name: 'Klaus',
  age: 23,

  // Symbol.toStringTag 是一个get访问器函数
  get [Symbol.toStringTag]() {
    // Symbol.toStringTag方法需要返回一个字符串类型的值
    // 如果返回的值的类型不是字符串类型,js会直接使用默认的type类型
    // 例如 如果这里的Symbol.toStringTag方法返回值为2, 是一个number类型值
    // 那么user.toString()的结果为 [object Object]
    // 也就是直接使用默认的type值 Object, 而不会尝试将2转换为'2'
    return this.name
  }
}

console.log(user.toString()) // => [object Klaus]

valueOf

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

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

// 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

```

> 总结:
> 
> 1.  `valueOf`和`toString` 都定义在`Object.prototype`上
>     
> 2.  `valueOf`会尝试将参数转换为对应的基本数据类型
>     
> 3.  对象类型调用`toString`方法的时候
>     
>     *   默认情况下,会根据对象的类型返回格式类似于`[object ${Type}]`的字符串
> 4.  某些对象会重写自身的`toString`方法和`valueOf`方法
>     

### 调用规则和优先级

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

```
// toPrimitive的伪代码形式如下:
toPrimitive(target, PreferredType = 'default': 'string' | 'number')

```

*   如果没有定义PreferredType, 则默认为`default`
    
*   如果PreferredType 的值为`default`,执行流程和PreferredType的值为`number`时的执行流程一致
    
*   如果PreferredType的值为`number`
    
    *   优先执行valueOf方法
        *   如果valueOf方法不存在或者返回的不是基本类型值,继续调用toString方法
        *   如果valueOf方法返回值是基本类型值,那么就直接返回基本类型值,不再继续调用toString方法
    *   如果调用了toString方法
        *   如果返回了基本数据类型值,就直接返回数据类型值
        *   如果没有返回基本数据类型值,就抛出异常

```
// PreferredType的值为number,优先调用valueOf方法
// 如果valueOf方法的返回值是基本数据类型值,则直接将该值转换为number类型值
const foo = {
  value: [],
  __proto__: {
    toString() {
      console.log('toString')
      return []
    },
    valueOf() {
      console.log('valueOf')
      return 2
    }
  }
}

console.log(Number(foo))
/*
  =>
    valueOf
    2
*/

// PreferredType的值为number,优先调用valueOf方法 // 如果valueOf方法的返回值不是基本数据类型值,则继续调用toString方法 // 如果toString方法返回值是基本数据类型值,则将基本数据类型转换为number类型值 const foo = { value: [], proto: { toString() { console.log('toString') return 2 }, valueOf() { console.log('valueOf') return [] } } }

console.log(Number(foo)) /* => valueOf toString 2 */

// PreferredType的值为number,优先调用valueOf方法
// 如果valueOf方法的返回值不是基本数据类型值,则继续调用toString方法
// 如果toString方法的返回值依旧不是一个基本数据类型值,则抛出一个异常,直接报错
const foo = {
  value: [],
  __proto__: {
    toString() {
      console.log('toString')
      return this.value
    },
    valueOf() {
      console.log('valueOf')
      return []
    }
  }
}

console.log(Number(foo))
/*
  =>
    valueOf
    toString
    error 抛出一个异常 ---  Cannot convert object to primitive values
*/

```

*   PreferredType的值为`string`
    *   优先执行toString方法,
        *   如果toString方法不存在或者返回的不是基本类型值,继续调用valueOf方法
        *   如果toString方法返回值是基本类型值,那么就直接返回基本类型值,不再继续调用valueOf方法
    *   如果调用了valueOf方法
        *   如果返回了基本数据类型值,就直接返回数据类型值
        *   如果没有返回基本数据类型值,就抛出异常

```
// PreferredType的值为string,优先调用toString方法
// 如果toString方法的返回值是基本数据类型值,则直接将该值转换为string类型值
const foo = {
  value: [],
  __proto__: {
    toString() {
      console.log('toString')
      return 2
    },
    valueOf() {
      console.log('valueOf')
      return []
    }
  }
}

console.log(String(foo))
/*
  =>
    toString
    '2'
*/

// PreferredType的值为string,优先调用toString方法 // 如果toString方法的返回值不是基本数据类型值,则继续调用valueOf方法 // 如果valueOf方法返回值是基本数据类型值,则将基本数据类型转换为string类型值 const foo = { value: [], proto: { toString() { console.log('toString') return [] }, valueOf() { console.log('valueOf') return 2 } } }

console.log(String(foo)) /* => toString valueOf '2' */

// PreferredType的值为string,优先调用toString方法
// 如果toString方法的返回值不是基本数据类型值,则继续调用valueOf方法
// 如果valueOf方法的返回值依旧不是一个基本数据类型值,则抛出一个异常,直接报错
const foo = {
  value: [],
  toString() {
    console.log('toString')
    return this.value
  },
  valueOf() {
    console.log('valueOf')
    return []
  }
}

console.log(String(foo))
/* 
  =>
    toString
    valueOf
    error 抛出一个异常 ---  Cannot convert object to primitive value
*/

```

​

### \[Symbol.toPrimitive\]

`Symbol.toPrimitive`是一个内置的Symbol值,所对应的是一个函数

当一个对象转换为对应的原始值时,会调用此函数

如果实现了`Symbol.toPrimitive`方法,JS就会使用`Symbol.toPrimitive`来将引用类型转换为基本数据类型

而不会再去调用原生的`valueOf`和`toString`

```
class Foo {
  constructor(num) {
    this.num = num
  }

  
  // 重写valueOf方法
  valueOf() {
    console.log('valueOf')
    return this.num
  }

  // 重写toString方法
  toString() {
    console.log('toString')
    return this.num + ''
  }

  // hint有三种取值可能性 string | number | default(缺省值)
  [Symbol.toPrimitive](hint) {
    if (hint === 'number') {
      // hint --> number
      return this.num
    } else if (hint === 'string') {
      // hint --> string
      return this.num + ''
    }

    // hint --> default
    return false
  }
}

// TEST CODE
const foo = new Foo(10)

console.log(Number(foo)) // => 10
console.log(String(foo)) // => '10'

console.log(`${foo}`) // => '10'
console.log(foo / 2) // => 5

// 注意: 比较运算和加法运算的时候,hint的值是default,并不是number
// 但是值为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

// 在将对象转换为boolean类型值时,会直接进行转换
// 而不会在此过程中调用valueOf方法 或 toString方法
console.log(!!foo) // => true

```

`某些对象会重写Object原型上的toString和valueOf方法,此时这些类型在转换为原始类型值时,就会有自己对应的转换规则`

```
// 保存原始的valueOf方法和toString方法
const valueOf = Object.prototype.valueOf
const toString = Object.prototype.toString

// 添加valueOf日志
Object.prototype.valueOf = function () {
  console.log('valueOf')
  return valueOf.call(this)
}

// 添加toString日志
Object.prototype.toString = function () {
  console.log('toString')
  return toString.call(this)
}

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方法和valueOf方法
// 对于Date实例而言,加法运算和判等运算 会调用Date.prototype.toString方法
// 而不是和普通对象那样去调用valueOf方法
console.log(date + 1)
console.log(date == 2)

```

### 示例

#### 如何让`a===1 && a===2 && a===3`和`a==1 && a==2 && a==3`的判断结果为true

判等可能会触发`隐式类型转换`,所以可以使用 `valueOf` 来实现

而全等并不会进行类型转换,只能通过`defineProperty`或`Proxy`来进行监听

```
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

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


> 本文使用 [文章同步助手](https://juejin.cn/post/6940875049587097631) 同步