How to add default values for custom function parameters in javascript

Source: Internet
Author: User
Tags comments eval regular expression wrapper hasownproperty

Recently on the codewars to see a very good topic, require JS to write a function defaultarguments, used to give certain parameters of the specified function to add a default value. For example:

The Foo function has an argument named X
var foo_ = defaultarguments (foo, {x:3});
Foo_ is a function that calls it returns the result of the Foo function, while x has a default value of 3

The following is a specific example:

function Add (A, b) {return a+b;}
Add default value to parameter B of the Add function 3
var add_ = defaultarguments (add, {b:3});
This code is equivalent to calling Add (2, 3), thus returning 5
add_ (2);//5
//And this code will not use the default value of B because it gives the value of a and B in its entirety, so return the
add_ (2, 10);//12


The reason is that this is a good topic, because it and those who simply test the algorithm of the problem, complete it requires you to JS a lot of knowledge points have a pretty deep understanding. This includes getting the function's parameter list, run-time arguments, regular expressions, higher-order functions, pipe calls, and other small points of knowledge.

I just got this topic when the feeling is not able to do, because I have not encountered a similar topic, there is no past experience can be used for reference. But after a simple thought, although there are many problems to be solved, but there is a preliminary thinking, the framework of this function should be basically this:

function defaultarguments (func, defaultvalues)  { 
 // step 1:  get formal parameter list   var argNames = ...;   //  returns a wrapper function that encapsulates a call to the original function   return function ()  {   
 // step 2:  Get run-time arguments     var args = ...;     // step 3:  undefined the arguments with default values, no default values are defined for the     // 
step 4:  calls the original function and returns the result     return func.apply (Null, args);
  }; }



The idea is quite clear, the function defaultarguments should return a function that wraps the call to the Func function, and then processes the incoming arguments before calling the Func function, replacing the arguments with the default values for those that are not passed in. Because the default values are specified with the parameter names rather than the order of the parameters in the list, you need to get a list of formal parameters to determine which arguments have the default values specified.


Step 1: Get the parameter list

The first thing to do when you start writing code is: How do you get a list of parameters for a function?

The question really scratching me, and finally came up with a method. We know that all of the objects in JS have the ToString () method, the function is an object, so what does the function object's ToString () return? Yes, that's the definition code for the function. For example, add.tostring () returns "function Add (A, b) {return a+b;}".

Get the string that defines the function, and get the method of the parameter list, just take the contents out of the brackets and split it with a comma (note that you want to remove the spaces before and after the parameter names).

(later read the problem description of the time to find the problem is a hint can be used function.tostring () method to get the formal parameter list, not seriously read the question of sadness ah. )

One way to remove the parameter list is to find the index of the left and right brackets, and then use SUBSTRING () to fetch it, and the other is to use regular expressions. I'm using regular expressions in the same way:

var match = func.tostring (). Match (/function [^\ (]*) \ ([^\)]*) \);
var argnames = Match[2].split (', ');


The matching procedure for this regular expression is shown in the following diagram:


The first group (group 1) is used to match the function name portion preceding the opening parenthesis, and the second grouping (Group 2) matches the list of formal parameters in parentheses. Note that both the function name and the parameter list are not required, so the * number is used when matching. The match () method returns an array in which the first element is the full result of the match, followed by the contents of each capturing grouping, so the Group 2 grouping that contains the parameter list corresponds to the third element that returns the result.

Step 2: Get run-time arguments

The formal parameter list is available, and the next step is to get the argument. Because the Func function is not defined by ourselves, we cannot refer to the argument with the parameter name, but JS implicitly provides a variable arguments for the invocation of each function to obtain the incoming argument. About arguments here is not much to say, I believe the JS are more understanding.

Step 3: To complement the argument with the default value

The first thing I did was, iterating the parameter group, if you find that the corresponding parameter value is undefined, check to see if the parameter is provided with a default value, and if it is, replace it with the default value. The code resembles the following:


var args = [];
for (var i = 0; i < argnames.length; i++) {
if (Arguments[i]!== undefined) {
Args[i] = arguments[i];
} else {
Args[i] = defaultvalues[argnames[i]];
}
}


But this code failed on one of the test cases. That use case explicitly passes in a undefined value. Like this: "Add_ (2, undefined);", this should return Nan, but my code replaces the B parameter with the default value of 3, so it returns 5.

I am aware that this method cannot be used to replace the default value of the parameter. When you think about it, you can provide the default value only the last number of parameters, you cannot provide a default value for one of the previous parameters and not a default value for the arguments that follow it. For example, for function add (a,b), it is not possible to provide a default value for parameter a only. If you call "Defaultarguments" (Add, {a:1}); Then, it seems like a has a default value of 1 and B has no default value, but in fact, B also implicitly has a default value of undefined, because you can never do only use the default value of a and pass a specific value to B.

For example, if you want to use the default value of a, and you want to pass 2 to B, this is not possible. If you do this: "Add_ (2)", it actually specifies a parameter value of 2 for a. And if you want to do this: "Add_ (Undefined, 2)", although it does pass 2 to B, it also assigns a undefined to a at the same time.

Therefore, the default parameter can only appear in the last few parameters of the parameter list. If we specify a default value for a parameter but are not specified for the argument that follows it, the default value for those parameters that are actually behind it is undefined. Just like A and B in the example above.

In fact, this rule has been known for a long time and has been used in other languages, but has not seriously considered the logic contained therein. It's not until the question is answered that you understand it thoroughly.

According to the above conclusion, you can easily modify the above code, just for the parameter list of the last number of parameters not passed the default value can be used, the original arguments of the value of the parameters do not need to go to it, even if the value of some of the parameters is undefined, which is the user's own incoming. You can therefore modify the code to:

var args = Array.prototype.slice.call (arguments);
for (var i = Arguments.length i < argnames.length; i++) {
Args[i] = defaultvalues[argnames[i]];
}


The complete code is as follows:

Var defaultarguments = function (func, defaultvalues)  {    if
  (!defaultvalues)  return func;
    var match = func.tostring (). Match (/function[^\]*\ ([^\)]*)/);     if  (!match | |
&NBSP;MATCH.LENGTH&NBSP;&LT;&NBSP;2)  return func;     var argnamestr = match[1].replace (/\s+/g,  ');  // remove
 spaces     if  (!ARGNAMESTR)  return func;
    var argnames = argnamestr.split (', ');     var wrapper = function ()  {      
  var args = array.prototype.slice.call (arguments);         for  (var i = arguments.length; i  < argnames.length; i++)  {            args[i] = defaultvalues[argnames[i]];         }         return 
Func.apply (Null, args);
    };
    return wrapper; }



This is the first program that I wrote according to the topic request, since thinks already good, several test cases which oneself writes also can pass smoothly, so the confidence clicked to submit, but ... Has failed. The failed use cases are probably like this:

var add_ = defaultarguments (add, {b:9});
add_ = Defaultarguments (add_, {a:2, b:3});
Add_ (10);

The result should be 13, because the default value for B is reset to 3, but the above program returns 19. The reason is that the function returned by the first call to Defaultarguments () has lost the parameter list of the Add function. The parameter list of the Add () function should be "a,b", but if we execute add_.tostring () after the first statement of the test case, we will return the definition of the wrapper function in defaultarguments instead of the Add, However, the wrapper function does not have a formal parameter defined.

In order to solve this problem, I have tried several kinds of schemes. First consider using the function class to construct a function object, because the constructor of the function accepts the parameter of the string type, so that you can define the parameters for the wrapper function because the parameter list of the Func function we have already got. The previous parameters of the function constructor are used to define the formal parameters, and the last argument is the function body. For example:

var add = new Function (' A ', ' B ', ' return a+b '); function (a,b) {return a+b}

But the question is, how can I pass the parameter list to the function's constructor? After all, the Func function is passed in by the user and the number of parameters is indeterminate, but the function constructor does not accept the array as an argument. The problem seems to be in deadlock, suddenly, I think if not new? Call the function's constructor as a normal function, so that I can use the Apply () method to pass an array as a parameter to it. After experimenting, it turns out that this is true, and the following code does return the same function object as when new is used:

Function.apply (null, [' A ', ' B ', ' return a+b ']);

So I started to reinvent my code in this way, but it didn't work because the internal wrapper function needed to use the Defaultarguments function's func and defaultvalues parameter values. However, function-constructed functions are in the global scope and cannot form closures in the current context. Blocked

Although the scheme failed, my understanding of the function constructor was a step forward, and it was a small gain.

The second scenario is to use eval () to construct a function object. The scheme was not implemented because I knew that eval () would also leave the constructed code out of the current scope and therefore not be able to form a closure, pass away.

The problem has again stalled. A number of options have been tried, but they are not workable. All of a sudden I brainwave, in fact, we don't need to make wrapper's formal parameter list consistent with Func, just let its ToString () return the same result as the Func parameter list, because we don't need real reflection wrapper itself, just through its ToString () to parse it. Therefore, we simply rewrite the wrapper toString () method to return the value of func.tostring ():

wrapper.tostring = function () {
return func.tostring ();
};

A code to solve the problem perfectly, in the mind really a little excited. So once again the confidence of a full Click to submit, I thought this time will be able to pass smoothly, but unfortunately again suffered a failure. The reason for this is this: when you include a comment in the parameter list of an incoming function, it causes the parsed parameter to be incorrect. For example:

function Add (A,//comments
b/* Note * * () {
return a + B;
}

At this point add.tostring () returned the string contains these annotations, if not processed, it will be the contents of the annotation incorrectly as part of the forming parameters, naturally is not. But this is a simple question, just remove the annotation after matching the content in parentheses, and use the appropriate regular expression to invoke replace () to:

var argnamestr = match[1].replace (/\/\/.*/gm, ')/remove Single-line comments
. replace (/\/\*.*?\*\//g, ')//Remove multi-line comments
. replace (/\s+/g, ""); Remove spaces

These two regular expressions are no longer to repeat. Revise and submit again, this time finally passed all the test cases! Sprinkle flowers ~ ~ ~

The complete procedure is as follows:

Var defaultarguments = function (func, defaultvalues)  {    if
  (!func)  return null;
    if  (!defaultvalues)  return func;
    var match = func.tostring (). Match (/function[^\]*\ ([^\)]*)/);     if  (!match | |
&NBSP;MATCH.LENGTH&NBSP;&LT;&NBSP;2)  return func;     var argnamestr = match[1].replace (/\/\/.*/gm,  ')  //  Remove single-line comments              Replace (/\/\*.*?\*\//g,  ')  // remove multi-line comments     
        .replace (/\s+/g,  ');  // remove spaces
    if  (!ARGNAMESTR)  return func;     var argnames = argnamestr.split (',');     var wrapper = function ()  {      
  var args = array.prototype.slice.call (arguments);         for  (var i = arguments.length; i  < argnames.length; i++)  {          
  args[i] = defaultValues[argNames[i]];         }         return 
Func.apply (Null, args);
    };     wrapper.tostring = function ()  {      
  return func.tostring ();
    };
    return wrapper; };



It's not over yet.

Although the final submission was successful, looking back and checking the code again, we found that there was a problem. For example, for the following code:

function Add (a,b,c) {return a+b+c;}
var add_ = defaultarguments (Add,{b:2,c:3});
Modify the default value for C, and note that the default value for B should still be 2
add_ = Defaultarguments (add_,{c:10});
Add_ (1);


Because the final B and C defaults are 2 and 10, the result of this code should be 13, but it actually is Nan.

The problem was not tested at the time of submission, and it appears that the test case for the original question is not perfect. To fix the problem, you need to find out the reason first. Let's take a look at what happens when you execute the code above.

This code calls a total of 2 times defaultarguments, so it generates 2 wrapper, and two wrapper is nested. We may as well call the first generation of wrapper called Wrapper1, the second is called Wrapper2. The relationship between them is shown in the following illustration:


When the last call to "add_ (1)", the parameter "1" is passed to Wrapper2, at which point a value of 1,b and C have no value. Then after Wrapper2, a new argument is formed: "1, Undefined, 10" and passed to Wrapper1. Note that at this point 3 parameters are valued, so they are not replaced with default values, so Wrapper1 will pass them directly to Func, so func returns "1+undefined+10", which is Nan. The entire process is shown in the following illustration:


Understand the reason, but how to solve it? My approach is to save the value of defaultvalues on wrapper, and the next time it is called, determine whether the default value specified before it exists, and if it exists, merge it into the new default value.


If you have previously saved a default value, take it out and merge it into a new defaultvalues
var _defaultvalues = func._defaultvalues;
if (_defaultvalues) {
For (var k in _defaultvalues) {
if (!defaultvalues.hasownproperty (k)) {
DEFAULTVALUES[K] = _defaultvalues[k];
}
}
}

......

After generating the wrapper, save the defaultvalues to the wrapper
Wrapper._defaultvalues = defaultvalues;


If you run it again, the 2-generation wrapper object will look like the following image:


The default parameter in Wrapper2 is no longer only c=10, but it merges the default values of B defined in wrapper1 so that there is no previous problem. In fact, this figure also shows that at this point the wrapper1 for wrapper2 is not very useful, because with the new default parameters, no longer need the default parameters in Wrapper1. The only use left for Wrapper1 is to eventually invoke the Func function. So if we save the initial func function and call it directly in Wrapper2, we can completely remove the wrapper1. This simply adds two lines of code:


If a saved Func function is removed, a layer of wrapper calls is omitted
Func = func._original? Func._original:func;

......

After generating the wrapper, save the Func function
Wrapper._original = func;


Then run the test code above, there is no longer wrapper1 in the Wrapper2. As shown in the following illustration:


At this point, I think the code has become perfect. The final code is as follows:

Var defaultarguments = function (func, defaultvalues)  {    if
  (!func)  return null;
    if  (!defaultvalues)  return func;     //  If you have previously saved a default value, take it out and merge it into the new defaultvalues     var _
defaultvalues = func._defaultvalues;     if  (_defaultvalues)  {        for   (var k in _defaultvalues)  {             if  (!defaultvalues.hasownproperty (k))  {       
         defaultValues[k] = _defaultValues[k];             }        &NBSP;&NBSP}    &nbsp}     //  If a saved func function is removed, omit a layer of wrapCalls to per     func = func._original ? func._original : func;
    var match = func.tostring (). Match (/function[^\]*\ ([^\)]*)/);     if  (!match | |
&NBSP;MATCH.LENGTH&NBSP;&LT;&NBSP;2)  return func;     var argnamestr = match[1].replace (/\/\/.*/gm,  ')  //  Remove single-line comments              Replace (/\/\*.*?\*\//g,  ')  // remove multi-line comments     
        .replace (/\s+/g,  ');  // remove spaces
    if  (!ARGNAMESTR)  return func;
    var argnames = argnamestr.split (', ');     var wrapper = function ()  {&NBSP;&NBSP;&NBSP;&NBSP;&NBSP;&NBsp;  var args = array.prototype.slice.call (arguments);         for  (var i = arguments.length; i  < argnames.length; i++)  {          
  args[i] = defaultValues[argNames[i]];         }         return 
Func.apply (Null, args);
    };     //  rewrite the wrapper tostring method to return the ToString () results of the original Func function      Wrapper.tostring = function ()  {        return 
Func.tostring ();
    };     //  Save the original Func function and the current default value object to wrapper     wrapper._original =
 func;
    wrapper._defaultValues = defaultValues;     return wrapper; };



Summary

The problem seems simple, but it's not easy to implement. Which involves a lot of knowledge in JS, and some are usually not very attention. Therefore, I am in the process of solving the problem while the experiment while looking at the data, and finally to fix the question, and my answer is better than many people's answer, the heart of the achievement is very high.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.