# 从规范角度剖析对象的原始值转换

当使用+运算符计算时,如果存在复杂数据类型,那么它将会被转换为基本数据类型进行计算。转换时会调用该对象上的valueOf/toString方法,这两个方法的返回值是转换后的结果。具体是调用valueOf还是toString。这取决内置的toPrimitive的调用结果。从主观上来说对象如果倾向转为number类型就调用valueOf,否则调用toString

valueOf/toString可以被开发者重写

const foo = {
    toString() {
        return 'lucas'
    },
    valueOf() {
        return 1
    }
}
1
2
3
4
5
6
7
8

TIP

这时候调用alert(foo)将输出lucas。因为alert()接收一个字符串参数。如果执行console.log(1+foo)将输出2

总结:valueOf偏向于运算,toString偏向于显示。

  • 在进行对象转换时,将优先调用toString方法,如若没有重写 toString,将调用 valueOf 方法;如果两个方法都没有重写,则按ObjecttoString输出。
  • 在进行强转字符串类型时,将优先调用 toString 方法,强转为数字时优先调用 valueOf
  • 使用运算操作符的情况下,valueOf的优先级高于toString

# Symbol.toPrimitive

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

class A {
    constructor(count) {
        this.count = count
    }
    valueOf() {
        return 2
    }
    toString() {
        return '哈哈哈'
    }
    // 我在这里
    [Symbol.toPrimitive](hint) {
        if (hint == "number") {
            return 10;
        }
        if (hint == "string") {
            return "Hello Libai";
        }
        return true;
    }
}

const a = new A(10)

console.log(`${a}`)     // 'Hello Libai' => (hint == "string")
console.log(String(a))  // 'Hello Libai' => (hint == "string")
console.log(+a)         // 10            => (hint == "number")
console.log(a * 20)     // 200           => (hint == "number")
console.log(a / 20)     // 0.5           => (hint == "number")
console.log(Number(a))  // 10            => (hint == "number")
console.log(a + '22')   // 'true22'      => (hint == "default")
console.log(a == 10)     // false        => (hint == "default")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

比较特殊的是+拼接符,这个属于default模式

  • 该方法作用同valueOf/toString()一样,但是优先级高于这两个
  • 该函数被调用时,会被传递一个字符串参数hint
    • string:字符串类型
    • number:数字类型
    • default:默认

# 面试题分析

完美呈现出toString/valueOf的作用

  • a===1&&a===2&&a===3什么时候为true

双等号(==):会触发隐式类型转换,所以可以使用 valueOf 或者 toString 来实现。每次判断都会触发valueOf方法,同时让value+1,才能使得下次判断成立。

class A {
    constructor(value) {
        this.value = value;
    }
    valueOf() {
        return this.value++;
    }
}
const a = new A(1);
if (a == 1 && a == 2 && a == 3) {
    console.log("Hi Libai!");
}
1
2
3
4
5
6
7
8
9
10
11
12

全等(===):严格等于不会进行隐式转换,这里使用 Object.defineProperty 数据劫持的方法来实现

let value = 1;
Object.defineProperty(window, 'a', {
    get() {
        return value++
    }
})
if (a === 1 && a === 2 && a === 3) {
    console.log("Hi Libai!")
}
1
2
3
4
5
6
7
8
9
  • 实现一个无限累加函数
add(1) // 1
add(1)(2) // 3
add(1)(2)(3) // 6
add(1)(2)(3)(4) // 10

// 以此类推
function add(a) {
    function sum(b) { // 使用闭包
        a = b ? a + b : a; // 累加
        return sum;
    }
    sum.toString = function() { // 只在最后一次调用
        return a;
    }
    return sum; // 返回一个函数
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • add函数内部定义sum函数并返回,实现连续调用
  • sum函数形成了一个闭包,每次调用进行累加值,再返回当前函数sum
  • add()每次都会返回一个函数sum,直到最后一个没被调用,默认会触发toString方法,所以我们这里重写toString方法,并返回累计的最终值a

TIP

add(10): 执行函数add(10),返回了sum函数,注意这一次没有调用sum,默认执行sum.toString方法。所以输出10

add(10)(20): 执行函数add(10),返回sum(此时a10),再执行sum(20),此时a30,返回sum,最后调用sum.toString()输出30add(10)(20)...(n)依次类推。

  • 柯里化实现多参累加
add(1)(3,4)(3,5)    // 16
add(2)(2)(3,5)      // 12
function add(){
    // 1 把所有参数转换成数组
    let args = Array.prototype.slice.call(arguments)
    // 2 再次调用add函数,传递合并当前与之前的参数
    let fn = function() {
        let arg_fn = Array.prototype.slice.call(arguments)
        return add.apply(null, args.concat(arg_fn))
    }
    // 3 最后默认调用,返回合并的值
    fn.toString = function() {
        return args.reduce(function(a, b) {
            return a + b
        })
    }
    return fn
}

// ES6写法
function add () {
    let args = [...arguments];
    let fn = function(){
        return add.apply(null, args.concat([...arguments]))
    } 
    fn.toString = () => args.reduce((a, b) => a + b)
    return fn;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28