大厂面试题常考:toString与valueOf如何深刻理解?结合大厂面试原题给你们作个分析

最近啊,有同窗来问,数据类型转换咱们都学过,但是在面试题中遇到了咱们怎么就总是不会用啊。javascript

讲到数据类型转换,咱们基本上都是讲到隐式数据类型转换和显式转换。讲到数据类型转换,咱们就要来聊聊,valueOf 和 toString 这两个方法了。前端

基本上,全部JS数据类型都拥有这两个方法,null除外。java

这两个方法都是在原型链上的方法,为了处理javascript值运算与显示的问题。面试

valueOf 和 toString 几乎都是在出现操做符(+-*/==><)时被调用(隐式转换)。数组

toString

 toString返回一个表示该对象的字符串,当对象表示为文本值或以指望的字符串方式被引用时,toString方法被自动调用。闭包

  1. 手动调用的效果

运行的效果,所有都转成了字符串。app

比较特殊的地方就是,在表示对象的时候,就会打印[object Object],表示数组的时候,就打印数组内容以逗号链接的字符串,至关于数组方法Array.join(',')方法。函数

let a = {} 
let b = [1, 2, 3] 
let c = '123' 
let d = function(){ console.log('fn') } 
console.log(b.toString())   // '1,2,3' 
console.log(a.toString())   // '[object Object]'
console.log(c.toString())   // '123' 
console.log(d.toString())   // 'function(){ console.log('fn') }'

2. 最精准的类型判断

 toString 有时候在某种场合会比使用 typeof & instanceof 判断更高效和准确些,属于更精确的判断方式测试

toString.call(()=>{})       // [object Function] 
toString.call([])           // [object Array] 
toString.call('')           // [object String] 
toString.call({})           // [object Object] 
toString.call(undefined)    // [object undefined] 
toString.call(null)         // [object null] 
toString.call(22)           // [object Number] 
toString.call(new Date)     // [object Date] 
toString.call(Math)         // [object Math] 
toString.call(window)       // [object Window]

3. 何时会自动调用

不少同窗会问了,这个toString方法,何时会自动调用呢?使用操做符的时候,若是其中一边为对象,则会先调用toSting方法,也就是隐式转换,而后再进行操做。this

let c = [1, 2, 3] 
let d = {a:2} 
Object.prototype.toString = function(){ 
    console.log('Object') 
} 
Array.prototype.toString = function(){ 
    console.log('Array') 
    return this.join(',')   // 返回toString的默认值(下面测试) 
} 
Number.prototype.toString = function(){ 
    console.log('Number') 
} 
String.prototype.toString = function(){ 
    console.log('String') 
} 
console.log(2 + 1)  // 3 
console.log('s')    // 's' 
console.log('s'+2)  // 's2' 
console.log(c < 2)  // false        (一次 => 'Array') 
console.log(c + c)  // "1,2,31,2,3" (两次 => 'Array') 
console.log(d > d)  // false        (两次 => 'Object')

4. 重写toString方法

既然知道了有 toString 这个默认方法,那咱们也能够来重写这个方法

class A {
    constructor(count) {
        this.count = count     
    }
    toString() {         
        return '我有这么多钱:' + this.count     
    } 
} 
let a = new A(100) 

console.log(a)              // A {count: 100} 
console.log(a.toString())   // 我有这么多钱:100 
console.log(a + 1)          // 我有这么多钱:1001

valueOf

valueOF这个方法,执行的时候返回当前对象的原始值。具体功能与toString大同小异,一样具备以上的自动调用和重写方法。

在下面的内容里,就不讲解其余的内容了。主要讲一下这二者的区别吧

let c = [1, 2, 3] 
let d = {a:2} 
console.log(c.valueOf())    // [1, 2, 3] 
console.log(d.valueOf())    // {a:2}

二者区别

共同点:在输出对象时会自动调用。

不一样点:默认返回值不一样,且存在优先级关系。

两者并存的状况下,在数值运算中,优先调用了valueOf,字符串运算中,优先调用了toString。

下面经过代码演示给你们看一下执行的原理效果

class A {
    valueOf() {
        return 2     
    }     
    toString() {
      return '哈哈哈'     
    } 
} 
let a = new A() 
console.log(String(a))  // '哈哈哈'   => (toString)
console.log(Number(a))  // 2         => (valueOf) 
console.log(a + '22')   // '222'     => (valueOf) 
console.log(a == 2)     // true      => (valueOf) 
console.log(a === 2)    // false     => (严格等于不会触发隐式转换)

执行的最后结果能够看出,若是转换为字符串时调用toString方法,若是是转换为数值时则调用valueOf方法。

但其中的 a + '22' 很不和谐,字符串合拼不是应该是调用toString方法吗?

为了验证这个问题,咱们须要作一次更加严谨的演示实验看看:

暂且先把 valueOf 方法去掉

class A {
    toString() {
    return '哈哈哈'     
    } 
} 
let a = new A()
console.log(String(a))  // '哈哈哈'     => (toString) 
console.log(Number(a))  // NaN         => (toString)
console.log(a + '22')   // '哈哈哈22'   => (toString) 
console.log(a == 2)     // false       => (toString)

去掉 toString 方法看看

class A {
    valueOf() {
        return 2     
    } 
} 
let a = new A() 
console.log(String(a))  // '[object Object]'    => (toString) 
console.log(Number(a))  // 2                    => (valueOf) 
console.log(a + '22')   // '222'                => (valueOf) 
console.log(a == 2)     // true                 => (valueOf)

对比以后发现有很大不一样吧。它没有像上面 toString 那样统一规整。

对于那个 [object Object],应该是从 Object 那里继承过来的,因此咱们再去掉它看看

class A {
    valueOf() {
        return 2     
    } 
}
let a = new A() 
Object.prototype.toString = null;
console.log(String(a))  // 2        => (valueOf) 
console.log(Number(a))  // 2        => (valueOf) 
console.log(a + '22')   // '222'    => (valueOf) 
console.log(a == 2)     // true     => (valueOf)

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

  1. 在进行对象转换时,将优先调用toString方法,如若没有重写 toString,将调用 valueOf 方法;若是两个方法都没有重写,则按Object的toString输出。
  2. 在进行强转字符串类型时,将优先调用 toString 方法,强转为数字时优先调用 valueOf。
  3. 使用运算操做符的状况下,valueOf的优先级高于toString。

[Symbol.toPrimitive]

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

看这个解释基本上不少同窗都是看不懂的,无论你有没有学过JS。

其实不用想的太复杂,就把他当作是一个函数就能够了。

  1. 做用:同valueOf()和toString()同样,可是优先级要高于这二者;

2.该函数被调用时,会被传递一个字符串参数hint,表示当前运算的模式

一共有三种模式:

string:字符串类型

number:数字类型

default:默认

下面来看看Symbol.toPrimitive在代码中的实现吧:

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")

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

划重点:此方法不兼容IE,这个重点我说出来都以为尴尬

面试题分析

讲完上面的原理比较,下面我来分享几道今年大厂的面试,题目是我学员给我提供的。

如下几道大厂必考的面试题,能够说很是完美的体现出 toString 与 valueOf 的代码做用了

1.如何让(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!");
}

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

let value = 1; 
Object.defineProperty(window, 'a', { 
get() {
    return value++     
} }) 
if (a === 1 && a === 2 && a === 3) { 
    console.log("Hi Libai!")
}

上面咱们就是劫持全局window上面的a,当a每一次作判断的时候都会触发get属性获取值,而且每一次获取值都会触发一次函数实行一次自增,判断三次就自增三次,因此最后会让公式成立。

  1. 如何实现一个无限累加函数

问题:用 JS 实现一个无限累加的函数 add,示例以下:

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; // 返回一个函数 
} 
add(1)              // 1
add(1)(2)           // 3
add(1)(2)(3)        // 6
add(1)(2)(3)(4)     // 10 
  1. add函数内部定义sum函数并返回,实现连续调用
  2. sum函数造成了一个闭包,每次调用进行累加值,再返回当前函数sum
  3. add()每次都会返回一个函数sum,直到最后一个没被调用,默认会触发toString方法,因此咱们这里重写toString方法,并返回累计的最终值a

这样说比较好理解:

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

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

3. 柯里化实现多参累加

这里是上面累加的升级版,实现多参数传递累加。

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; 
    }

好了,今天的内容就分享那么多,不知道对于 valueOf 和 toString方法你有没有更加深入的理解了呢?若是今天的分享教程你学到了东西,别忘了关注公众号【前端研究所】哦!平时也欢迎把你不懂的问题分享到公众号文章的评论留意,说不定下一次就来说解你的疑问了呢!