Recently I found a front-end JS interview question. After carefully reviewing this front-end JS interview question, I found that it is still very interesting, so I would like to share it with you.
Please implement a function so that the operation result can meet the following expected results:
add(1)(2) // 3
add(1 , 2, 3)(10) // 16
add(1)(2)(3)(4)(5) // 15
For a curious Qie For Tuzai, I couldn't help but give it a try. When I saw the question, the first thing I thought of was the use of higher-order functions and Array.prototype.reduce()
.
Recommended related articles:The most complete collection of js interview questions in 2020 (latest)
Higher-order function: A higher-order function means that it receives another function as a parameter. In JavaScript, functions are first-class citizens, allowing functions to be passed as parameters or return values.
Got the following solution:
function add() { var args = Array.prototype.slice.call(arguments); return function() { var arg2 = Array.prototype.slice.call(arguments); return args.concat(arg2).reduce(function(a, b){ return a + b; }); } }
After verification, I found it was wrong:
add(1)(2) // 3 add(1, 2)(3) // 6 add(1)(2)(3) // Uncaught TypeError: add(...)(...) is not a function(…)
The above solution is correct only in the case of add()() . When the parameters of the chain operation are more than two or less than two, the result cannot be returned.
And this is also a difficulty in this question. When adding(), how to return both a value and a function for subsequent calls?
Later, with the guidance of an expert, one of the solutions can be obtained by rewriting the valueOf method or toString method of the function:
function add () { var args = Array.prototype.slice.call(arguments); var fn = function () { var arg_fn = Array.prototype.slice.call(arguments); return add.apply(null, args.concat(arg_fn)); } fn.valueOf = function () { return args.reduce(function(a, b) { return a + b; }) } return fn; }
Huh? When I first saw this solution, I was confused. Because I feel that fn.valueOf() has never been called from beginning to end, but I verified the result:
add(1) // 1 add(1,2)(3) //6 add(1)(2)(3)(4)(5) // 15
Miraculously right! Then the mystery must be in the fn.valueOf = function() {} above. Why is this so? At what point in the function is this method executed? Just listen to me step by step.
valueOf
and toString
Let’s briefly understand these two methods:
Object.prototype.valueOf()
In the words of MDN, The valueOf() method returns the original value of the specified object.
JavaScript calls the valueOf() method to convert an object into a primitive type of value (numeric, string, and Boolean). But we rarely need to call this function ourselves. The valueOf method is usually automatically called by JavaScript.
Remember the above sentence, below we will elaborate on what the so-called automatic call means.
Object.prototype.toString()
toString()
The method returns a string representing the object.
Every object has a toString() method, which is automatically called when the object is represented as a text value or when the object is referenced in a way that expects a string.
Remember here that valueOf() and toString() will call themselves on specific occasions.
Primitive types
Okay, to set the stage, let’s first understand the several primitive types of JavaScript. Apart from Object and Symbol, there are the following primitive types:
Number String Boolean Undefined Null
In JavaScript When performing comparison or various operations, the object will be converted into these types for subsequent operations. The following are explained one by one:
String type conversion
In a certain When an operation or calculation requires a string and the object is not a string, the String conversion of the object will be triggered, and the non-string type will be automatically converted to the String type. The toString function is automatically called internally by the system. For example:
var obj = {name: 'Coco'}; var str = '123' + obj; console.log(str); // 123[object Object]
Conversion rules:
If the toString method exists and returns the original type, return the result of toString.
If the toString method does not exist or the returned type is not a primitive type, call the valueOf method. If the valueOf method exists and returns primitive type data, return the result of valueOf.
In other cases, an error is thrown.
The above example is actually:
var obj = {name: 'Coco'}; var str = '123' + obj.toString();
Among them, the value of obj.toString() is "[object Object]".
Assume it is an array:
var arr = [1, 2]; var str = '123' + arr; console.log(str); // 1231,2
The above + arr, since this is a string addition operation, the following arr needs to be converted into a string type, so + arr.toString(() is actually called ).
However, we can rewrite the toString and valueOf methods of the object ourselves:
var obj = { toString: function() { console.log('调用了 obj.toString'); return {}; }, valueOf: function() { console.log('调用了 obj.valueOf') return '110'; } } alert(obj); // 调用了 obj.toString // 调用了 obj.valueOf // 110
Alert(obj + '1') above, obj will automatically call its own obj.toString() method to convert For primitive types, if we do not override its toString method, [object Object]1 will be output. Here we override toString and return a primitive type string 111, so the final alert is 1111.
The above conversion rules are written, the toString method needs to exist and return the original type, then if the returned type is not a primitive type, it will continue to look for the valueOf method of the object:
Let’s try next Demonstrates what happens if the toString() method is not available while an object is trying to be converted to a string.
At this time, the system will call the valueOf() method again. Next, we rewrite the object's toString and valueOf:
var obj = { toString: function() { console.log('调用了 obj.toString'); return {}; }, valueOf: function() { console.log('调用了 obj.valueOf') return '110'; } } alert(obj); // 调用了 obj.toString // 调用了 obj.valueOf // 110
从结果可以看到,当 toString 不可用的时候,系统会再尝试 valueOf 方法,如果 valueOf 方法存在,并且返回原始类型(String、Number、Boolean)数据,返回valueOf的结果。
那么如果,toString 和 valueOf 返回的都不是原始类型呢?看下面这个例子:
var obj = { toString: function() { console.log('调用了 obj.toString'); return {}; }, valueOf: function() { console.log('调用了 obj.valueOf') return {}; } } alert(obj); // 调用了 obj.toString // 调用了 obj.valueOf // Uncaught TypeError: Cannot convert object to primitive value
可以发现,如果 toString 和 valueOf 方法均不可用的情况下,系统会直接返回一个错误。
在查证了 ECMAScript5 官方文档后,发现上面的描述有一点问题,Object 类型转换为 String 类型的转换规则远比上面复杂。转换规则为:1.设原始值为调用 ToPrimitive 的结果;2.返回 ToString(原始值) 。关于 ToPrimitive 和 ToString 的规则可以看看官方文档:ECMAScript5 — ToString
Number 类型转换
上面描述的是 String 类型的转换,很多时候也会发生 Number 类型的转换:
调用 Number() 函数,强制进行 Number 类型转换
调用 Math.sqrt() 这类参数需要 Number 类型的方法
obj == 1 ,进行对比的时候
obj + 1 , 进行运算的时候
与 String 类型转换相似,但是 Number 类型刚好反过来,先查询自身的 valueOf 方法,再查询自己 toString 方法:
如果 valueOf 存在,且返回原始类型数据,返回 valueOf 的结果。
如果 toString 存在,且返回原始类型数据,返回 toString 的结果。
其他情况,抛出错误。
按照上述步骤,分别尝试一下:
var obj = { valueOf: function() { console.log('调用 valueOf'); return 5; } } console.log(obj + 1); // 调用 valueOf // 6 var obj = { valueOf: function() { console.log('调用 valueOf'); return {}; }, toString: function() { console.log('调用 toString'); return 10; } } console.log(obj + 1); // 调用 valueOf // 调用 toString // 11 var obj = { valueOf: function() { console.log('调用 valueOf'); return {}; }, toString: function() { console.log('调用 toString'); return {}; } } console.log(obj + 1); // 调用 valueOf // 调用 toString // Uncaught TypeError: Cannot convert object to primitive value
Boolean 转换
什么时候会进行布尔转换呢:
布尔比较时
if(obj) , while(obj) 等判断时
简单来说,除了下述 6 个值转换结果为 false,其他全部为 true:
undefined
null
-0
0或+0
NaN
Boolean(undefined) // false Boolean(null) // false Boolean(0) // false Boolean(NaN) // false Boolean('') // false
Function 转换
好,最后回到我们一开始的题目,来讲讲函数的转换。
我们定义一个函数如下:
function test() { var a = 1; console.log(1); }
如果我们仅仅是调用 test 而不是 test() ,看看会发生什么?
可以看到,这里把我们定义的 test 函数的重新打印了一遍,其实,这里自行调用了函数的 valueOf 方法:
我们改写一下 test 函数的 valueOf 方法。
test.valueOf = function() { console.log('调用 valueOf 方法'); return 2; } test; // 输出如下: // 调用 valueOf 方法 // 2
与 Number 转换类似,如果函数的 valueOf 方法返回的不是一个原始类型,会继续找到它的 toString 方法:
test.valueOf = function() { console.log('调用 valueOf 方法'); return {}; } test.toString= function() { console.log('调用 toString 方法'); return 3; } test; // 输出如下: // 调用 valueOf 方法 // 调用 toString 方法 // 3
破题
再看回我正文开头那题的答案,正是运用了函数会自行调用 valueOf 方法这个技巧,并改写了该方法。我们稍作改变,变形如下:
function add () { console.log('进入add'); var args = Array.prototype.slice.call(arguments); var fn = function () { var arg_fn = Array.prototype.slice.call(arguments); console.log('调用fn'); return add.apply(null, args.concat(arg_fn)); } fn.valueOf = function () { console.log('调用valueOf'); return args.reduce(function(a, b) { return a + b; }) } return fn; }
当调用一次 add 的时候,实际是是返回 fn 这个 function,实际是也就是返回 fn.valueOf();
add(1); // 输出如下: // 进入add // 调用valueOf // 1 其实也就是相当于: [1].reduce(function(a, b) { return a + b; }) // 1
当链式调用两次的时候:
add(1)(2); // 输出如下: // 进入add // 调用fn // 进入add // 调用valueOf // 3
当链式调用三次的时候:
add(1)(2)(3); // 输出如下: // 进入add // 调用fn // 进入add // 调用fn // 进入add // 调用valueOf // 6
可以看到,这里其实有一种循环。只有最后一次调用才真正调用到 valueOf,而之前的操作都是合并参数,递归调用本身,由于最后一次调用返回的是一个 fn 函数,所以最终调用了函数的 fn.valueOf,并且利用了 reduce 方法对所有参数求和。
除了改写 valueOf 方法,也可以改写 toString 方法,所以,如果你喜欢,下面这样也可以:
function add () { var args = Array.prototype.slice.call(arguments); var fn = function () { var arg_fn = Array.prototype.slice.call(arguments); return add.apply(null, args.concat(arg_fn)); } fn.toString = function() { return args.reduce(function(a, b) { return a + b; }) } return fn; }
这里有个规律,如果只改写 valueOf() 或是 toString() 其中一个,会优先调用被改写了的方法,而如果两个同时改写,则会像 Number 类型转换规则一样,优先查询 valueOf() 方法,在 valueOf() 方法返回的是非原始类型的情况下再查询 toString() 方法。
后记
在尝试了更多的浏览器之后,发现了上述解法的诸多问题,在 chrome 56 55 下,结果正常。在更新到最新的 chrome57 ,控制台下,结果都会带上 function 字段,在 firefox 下,直接不生效,感觉自己可能陷入了追求某种解法而忽略了一些底层的具体规范,会在彻底弄清楚后给出另一篇文章。
对于类型转换,最好还是看看 ECMAScript 规范,拒绝成为伸手党,自己多尝试。另外评论处有很多人提出了自己的疑问,值得一看。
正如俗话所说:“炫耀从来不是我写作的动机,好奇才是”。此前端JS面试题的解读也是我自己学习的一个过程,过程中我也遇到了很多困惑,所以即便查阅了官方文档及大量的文章,但是错误及疏漏仍然在所难免,欢迎指正及给出更好的方法。
到此本文结束,如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。
相关推荐:
What is JavaScript? How to use JavaScript?
JavaScript converts the Date object into String and return the result toGMTString()
The above is the detailed content of Front-end JS interview questions. For more information, please follow other related articles on the PHP Chinese website!