Thoughts on javascript type conversion caused by an interview question

  • 2021-07-26 06:47:45
  • OfStack

Recently, someone in the group sent the following question:

Implement a function, and 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 cutter, I couldn't help but try it once. When I saw the topic, the first thing that came to mind was to use higher-order functions and Array. prototype. reduce ()

Higher order function (Higher-order function): Higher order function means that it takes another function as an argument. In javascript, functions are citizens of class 1, allowing functions to be passed as arguments or return values.

The following solution is obtained:


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

Verified 1 times and found an error:


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 () (). However, when the chain operation has more than two parameters or less than two parameters, the result cannot be returned.

And this is also a difficult point of this problem. When add (), how to return both a value and a function for subsequent calls?

Later, after being instructed by an expert, one solution 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;
}

Hmm? When I first saw this solution, I was overwhelmed. Because I feel that fn. valueOf () has not been called from beginning to end, but I validate the following results:


add(1) // 1
add(1,2)(3) //6
add(1)(2)(3)(4)(5) // 15

Amazing right! Then the mystery must be in fn. valueOf = function () {} above. Why is this the case? At what point is this method executed in the function? Listen to me step by step.

valueOf and toString

Let's take a brief look at 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 objects to values of their original types (numeric, string, and Boolean). But we rarely need to call this function ourselves, and valueOf method 1 is usually called automatically by JavaScript.

With the above sentence in mind, we will elaborate on what the so-called automatic call means.

Object.prototype.toString()

The toString () method returns a string representing the object.

Each object has an toString () method, which is automatically invoked when the object is represented as a text value or when the object is referenced as a desired string.

Remember here that valueOf () and toString () are called on their own in certain situations.

Primitive type

Ok, paving the way for 1, first understand several original types of javascript, except Object and Symbol, there are the following original types:

Number String Boolean Undefined Null

When JavaScript compares or performs various operations, it will convert objects into these types, so as to carry out subsequent operations, which are described one by one below:

String type conversion

When a string is needed for an operation or operation, the String conversion of Object will be triggered, 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, the result of toString is returned. If the toString method does not exist or returns a "primitive type", call the valueOf method, and 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();

Where obj. toString () has a value of "[object Object]".

Assuming an array:


var arr = [1, 2];
var str = '123' + arr;
console.log(str); // 1231,2

The + arr above actually calls + arr. toString ().

However, we can rewrite the toString, valueOf methods of the object ourselves:


var obj = {
  toString: function() {
    console.log(' Called  obj.toString');
    return '111';
  }
}
alert(obj);
//  Called  obj.toString
// 111

Above alert (obj), obj will automatically call its own obj. toString () method to convert to the original type, and if we don't override its toString method, it will output [object Object]. Here we override toString and return a original type string 111, so alert finally comes out with 111.

The above conversion rule states that the toString method needs to exist and return the original type, so if it does not return 1 original type, it will continue to look for the valueOf method of the object:

Let's try to prove that the system calls the valueOf () method if the toString () method is not available. Let's overwrite the valueOf of the object:


var obj = {
  toString: function() {
    console.log(' Called  obj.toString');
    return {};
  },
  valueOf: function() {
    console.log(' Called  obj.valueOf')
    return '110';
  }
}
alert(obj);
//  Called  obj.toString
//  Called  obj.valueOf
// 110

It can be seen from the results that when toString is unavailable, the system will try the valueOf method again. If the valueOf method exists and returns the original type (String, Number, Boolean) data, the result of valueOf will be returned.

What if neither toString nor valueOf returned the original type? Look at this example:


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

It can be found that if both toString and valueOf methods are not available, the system will directly return an error.

Number type conversion

What is described above is the conversion of String type, and the conversion of Number type also occurs in many cases:

Call the Number () function to force Number type conversion Calling parameters like Math. sqrt () requires a method of type Number obj = = 1, when comparing obj + 1, when performing operations

Similar to String type conversion, but the Number type is just the opposite, querying its own valueOf method first and then its own toString method:

If valueOf exists and the original type data is returned, the result of valueOf is returned. If toString exists and the original type data is returned, the result of toString is returned. In other cases, an error is thrown.

Follow the above steps and try 1 time respectively:


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

Boolean conversion

When will Boolean conversion be performed:

Boolean comparison time When judging if (obj), while (obj), etc.

Simply put, except for the following six values, the conversion result is false, and all others are true:

undefined null -0 0 or +0 NaN ''(empty string)

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

Function Conversion

Ok, finally, back to our topic at the beginning of 1, let's talk about the transformation of functions.

We define a function as follows:


function test() {
  var a = 1;
  console.log(1);
}

What happens if we just call test instead of test ()?

As you can see, the test function we defined was reprinted once. In fact, the valueOf method of the function was called by itself:

We rewrite the valueOf method of test function in 1.


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

Similar to the Number transformation, if the function's valueOf method does not return a primitive type, it continues to find its toString method:


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

Break a problem

Looking back at the answer to the question at the beginning of my text, I used the skill that the function will call the valueOf method by itself and rewrote the method. We make a slight change and the deformation is as follows:


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

When calling add once, it actually returns fn, which is function, and actually returns fn. valueOf ();


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

In fact, it is equivalent to:


[1].reduce(function(a, b) {
  return a + b;
})
// 1

When the chain is called twice:


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

When the chain is called 3 times:


add(1)(2)(3);
//  The output is as follows: 
//  Enter add
//  Call fn
//  Enter add
//  Call fn
//  Enter add
//  Call valueOf
// 6

As you can see, there is actually a cycle here. Only the last call really calls valueOf, and the previous operations are all merging parameters, recursively calling itself. Because the last call returns an fn function, the fn. valueOf of the function is finally called, and the reduce method is used to sum all parameters.

In addition to overwriting the valueOf method, you can also overwrite the toString method, so if you like, the following can also be done:


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

As a rule, if only one of valueOf () or toString () is overwritten, the overwritten method will be called first, and if both are overwritten at the same time, the valueOf () method will be queried first, and the toString () method will be queried again if the valueOf () method returns a non-original type, just like String conversion rule 1.

If you can read it carefully, I believe you will gain something.


Related articles: