全面分析toString与valueOf,并随手解决掉几道大厂必备面试题

开发 前端
基本上,所有JS数据类型都拥有这两个方法,null除外。它们俩是位于原型链上的方法,也是为了解决javascript值运算与显示的问题。

[[343896]]

基本上,所有JS数据类型都拥有这两个方法,null除外。它们俩是位于原型链上的方法,也是为了解决javascript值运算与显示的问题。

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

toString

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

1. 手动调用看看什么效果

嗯,跟介绍的一样,没骗人,全部都转成了字符串。

比较特殊的地方就是,表示对象的时候,变成[object Object],表示数组的时候,就变成数组内容以逗号连接的字符串,相当于Array.join(',')。 

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

2. 最精准的类型判断

这种属于更精确的判断方式,在某种场合会比使用 typeof & instanceof 来的更高效和准确些。 

  1. toString.call(()=>{})       // [object Function]  
  2. toString.call({})           // [object Object]  
  3. toString.call([])           // [object Array]  
  4. toString.call('')           // [object String]  
  5. toString.call(22)           // [object Number]  
  6. toString.call(undefined)    // [object undefined]  
  7. toString.call(null)         // [object null]  
  8. toString.call(new Date)     // [object Date]  
  9. toString.call(Math)         // [object Math]  
  10. toString.call(window)       // [object Window] 

3. 什么时候会自动调用呢

使用操作符的时候,如果其中一边为对象,则会先调用toSting方法,也就是隐式转换,然后再进行操作。 

  1. let c = [1, 2, 3]  
  2. let d = {a:2}  
  3. Object.prototype.toString = function(){  
  4.     console.log('Object')  
  5.  
  6. Array.prototype.toString = function(){  
  7.     console.log('Array')  
  8.     return this.join(',')   // 返回toString的默认值(下面测试)  
  9.  
  10. Number.prototype.toString = function(){  
  11.     console.log('Number')  
  12.  
  13. String.prototype.toString = function(){  
  14.     console.log('String')  
  15.  
  16. console.log(2 + 1)  // 3  
  17. console.log('s')    // 's'  
  18. console.log('s'+2)  // 's2'  
  19. console.log(c < 2)  // false        (一次 => 'Array')  
  20. console.log(c + c)  // "1,2,31,2,3" (两次 => 'Array')  
  21. console.log(d > d)  // false        (两次 => 'Object') 

4. 重写toString方法

既然知道了有 toString 这个默认方法,那我们也可以来重写这个方法 

  1. class A {  
  2.     constructor(count) {  
  3.         this.count = count  
  4.     }  
  5.     toString() {  
  6.         return '我有这么多钱:' + this.count  
  7.     }  
  8.  
  9. let a = new A(100)  
  10. console.log(a)              // A {count: 100}  
  11. console.log(a.toString())   // 我有这么多钱:100  
  12. console.log(a + 1)          // 我有这么多钱:1001 

Nice.

valueOf

返回当前对象的原始值。

具体功能与toString大同小异,同样具有以上的自动调用和重写方法。

这里就没什么好说的了,主要为两者间的区别,有请继续往下看🙊🙊 

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

两者区别

  •  共同点:在输出对象时会自动调用。
  •  不同点:默认返回值不同,且存在优先级关系。

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

看代码方可知晓: 

  1. class A {  
  2.     valueOf() {  
  3.         return 2  
  4.     }  
  5.     toString() {  
  6.         return '哈哈哈'  
  7.     }  
  8.  
  9. let a = new A()  
  10. console.log(String(a))  // '哈哈哈'   => (toString)  
  11. console.log(Number(a))  // 2         => (valueOf)  
  12. console.log(a + '22')   // '222'     => (valueOf)  
  13. console.log(a == 2)     // true      => (valueOf)  
  14. console.log(a === 2)    // false     => (严格等于不会触发隐式转换) 

结果给人的感觉是,如果转换为字符串时调用toString方法,如果是转换为数值时则调用valueOf方法。

但其中的 a + '22' 很不和谐,字符串合拼应该是调用toString方法。为了追究真相,我们需要更严谨的实验。

  •  暂且先把 valueOf 方法去掉 
  1. class A {  
  2.     toString() {  
  3.         return '哈哈哈'  
  4.     }  
  5.  
  6. let a = new A()  
  7. console.log(String(a))  // '哈哈哈'     => (toString)  
  8. console.log(Number(a))  // NaN         => (toString)  
  9. console.log(a + '22')   // '哈哈哈22'   => (toString)  
  10. console.log(a == 2)     // false       => (toString) 
  •  去掉 toString 方法看看 
  1. class A {  
  2.     valueOf() {  
  3.         return 2  
  4.     }  
  5.  
  6. let a = new A()  
  7. console.log(String(a))  // '[object Object]'    => (toString)  
  8. console.log(Number(a))  // 2                    => (valueOf)  
  9. console.log(a + '22')   // '222'                => (valueOf)  
  10. console.log(a == 2)     // true                 => (valueOf) 

发现有点不同吧?!它没有像上面 toString 那样统一规整。对于那个 [object Object],我估计是从 Object 那里继承过来的,我们再去掉它看看。 

  1. class A {  
  2.     valueOf() {  
  3.         return 2  
  4.     }  
  5.  
  6. let a = new A()  
  7. Object.prototype.toString = null;   
  8. console.log(String(a))  // 2        => (valueOf)  
  9. console.log(Number(a))  // 2        => (valueOf)  
  10. console.log(a + '22')   // '222'    => (valueOf)  
  11. 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 值,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数。

是不是有点懵???把它当做一个函数就行了~~

  •  作用:同valueOf()和toString()一样,但是优先级要高于这两者;
  •  该函数被调用时,会被传递一个字符串参数

    hint

    ,表示当前运算的模式,一共有三种模式:

  •  string:字符串类型
  •  number:数字类型
  •  default:默认

下面来看看实现吧: 

  1. class A {  
  2.     constructor(count) { 
  3.          this.count = count  
  4.     }  
  5.     valueOf() {  
  6.         return 2  
  7.     }  
  8.     toString() {  
  9.         return '哈哈哈'  
  10.     }  
  11.     // 我在这里  
  12.     [Symbol.toPrimitive](hint) {  
  13.         if (hint == "number") { 
  14.              return 10;  
  15.         }  
  16.         if (hint == "string") {  
  17.             return "Hello Libai";  
  18.         }  
  19.         return true;  
  20.     }  
  21.  
  22. const a = new A(10)  
  23. console.log(`${a}`)     // 'Hello Libai' => (hint == "string")  
  24. console.log(String(a))  // 'Hello Libai' => (hint == "string")  
  25. console.log(+a)         // 10            => (hint == "number")  
  26. console.log(a * 20)     // 200           => (hint == "number") 
  27. console.log(a / 20)     // 0.5           => (hint == "number")  
  28. console.log(Number(a))  // 10            => (hint == "number")  
  29. console.log(a + '22')   // 'true22'      => (hint == "default")  
  30. console.log(a == 10)     // false        => (hint == "default") 

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

划重点:此方法不兼容IE,尴尬到我不想写出来了~~

面试题分析

以下几道大厂必考的面试题,完美呈现出 toString 与 valueOf 的作用。

1. a===1&&a===2&&a===3 为 true

双等号(==):会触发隐式类型转换,所以可以使用 valueOf 或者 toString 来实现。

每次判断都会触发valueOf方法,同时让value+1,才能使得下次判断成立。 

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

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

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

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

  •  注:defineProperty 可参考这篇文章学习,点我进入传送门
  •  自:大厂面试题分享:如何让(a===1&&a===2&&a===3)的值为true?

2. 实现一个无限累加函数

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

  1. add(1); // 1  
  2. add(1)(2);  // 3  
  3. add(1)(2)(3); // 6  
  4. add(1)(2)(3)(4); // 10   
  5. // 以此类推  
  6. function add(a) {  
  7.     function sum(b) { // 使用闭包  
  8.         a = b ? a + b : a; // 累加  
  9.         return sum;  
  10.     }  
  11.     sum.toString = function() { // 只在最后一次调用  
  12.         return a;  
  13.     }  
  14.     return sum; // 返回一个函数  
  15.  
  16. add(1)              // 1  
  17. add(1)(2)           // 3  
  18. add(1)(2)(3)        // 6  
  19. add(1)(2)(3)(4)     // 10  
  •  add函数内部定义sum函数并返回,实现连续调用
  •  sum函数形成了一个闭包,每次调用进行累加值,再返回当前函数sum
  •  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. 柯里化实现多参累加

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

  1. add(1)(3,4)(3,5)    // 16  
  2. add(2)(2)(3,5)      // 12  
  3. function add(){  
  4.     // 1 把所有参数转换成数组  
  5.     let args = Array.prototype.slice.call(arguments)  
  6.     // 2 再次调用add函数,传递合并当前与之前的参数  
  7.     let fn = function() {  
  8.         let arg_fn = Array.prototype.slice.call(arguments)  
  9.         return add.apply(null, args.concat(arg_fn))  
  10.     }  
  11.     // 3 最后默认调用,返回合并的值  
  12.     fn.toString = function() {  
  13.         return args.reduce(function(a, b) {  
  14.             return a + b  
  15.         })  
  16.     }  
  17.     return fn  
  18.  
  19. // ES6写法  
  20. function add () {  
  21.     let args = [...arguments];  
  22.     let fn = function(){  
  23.         return add.apply(null, args.concat([...arguments]))  
  24.     }  
  25.      fn.toString = () => args.reduce((a, b) => a + b)  
  26.     return fn;  

 

 

责任编辑:庞桂玉 来源: 前端大全
相关推荐

2019-12-26 09:52:33

Redis集群线程

2019-07-18 15:42:53

Redisoffer数据库

2021-07-30 14:18:05

MongoDB数据库面试

2023-12-05 08:18:51

函数valueOf

2019-09-10 10:48:10

RedisJava面试题

2022-03-31 09:50:45

JS面试题

2020-01-18 07:55:28

JavaScript开发

2021-06-27 22:48:28

Redis数据库内存

2020-01-13 07:50:58

JavaScript开发

2020-08-06 10:45:30

JavaSpring面试题

2021-12-08 11:18:21

Spring Bean面试题生命周期

2020-06-24 09:55:17

Web面试前端

2020-06-04 14:40:40

面试题Vue前端

2015-07-13 09:45:32

阿里校招

2015-06-25 09:49:25

JavaScript隐式调用

2023-11-13 07:37:36

JS面试题线程

2011-03-24 13:27:37

SQL

2013-11-01 09:27:48

Twitter技术面试

2018-01-02 09:23:38

数据分析算法阿里巴巴

2019-02-18 13:36:03

Redis数据库面试
点赞
收藏

51CTO技术栈公众号