Get function's default value?
Asked Answered
K

5

15

Is there a way to retrieve a function's default argument value in JavaScript?

function foo(x = 5) {
    // things I do not control
}

Is there a way to get the default value of x here? Optimally, something like:

getDefaultValues(foo); // {"x": 5}

Note that toStringing the function would not work as it would break on defaults that are not constant.

Kriemhild answered 4/10, 2015 at 14:35 Comment(5)
Python, Ruby and C# all do this by the way.Kriemhild
check this #895360Mag
@OrBachar That is the question of set default parameter, here OP want to get default parameter value from outside of the function without calling it.Capsule
what is the use case ?Bearnard
What are you trying to do? This reminds me of Building a LINQ-like query API in JavaScriptUnyoke
T
6

Since we don't have classic reflection in JS, as you can find on C#, Ruby, etc., we have to rely on one of my favorite tools, regular expressions, to do this job for us:

let b = "foo";
function fn (x = 10, /* woah */ y = 20, z, a = b) { /* ... */ }

fn.toString()
  .match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1] // Get the parameters declaration between the parenthesis
  .replace(/(\/\*[\s\S]*?\*\/)/mg,'')             // Get rid of comments
  .split(',')
  .reduce(function (parameters, param) {          // Convert it into an object
    param = param.match(/([_$a-zA-Z][^=]*)(?:=([^=]+))?/); // Split parameter name from value
    parameters[param[1].trim()] = eval(param[2]); // Eval each default value, to get strings, variable refs, etc.

    return parameters;
  }, {});

// Object { x: 10, y: 20, z: undefined, a: "foo" }

If you're going to use this, just make sure you're caching the regexs for performance.

Thanks to bubersson for hints on the first two regexs

Trocki answered 5/10, 2015 at 8:6 Comment(8)
This breaks if the environment isn't the same: function foo(){ var x = 5; return function bar(y = x){}}; var bar = foo();Kriemhild
Not true. You could still use .toString() on that returned function.Trocki
No, because you would have to evaluate it, and that would cause a side effect. For example function foo(){ var x = prompt("Please enter a number"); return function (y = x) {}; } var bar = foo() - how would you extract the value y is bound to here?Kriemhild
I understand, but how do you expect to get around an inherent limitation of the language? It's as if you'd ask "how to get a scoped variable outside of its closure"Trocki
Well, hopefully there is an API in ES2015 or ES2016 or something planned (or a reason it was rejected) that lets you do Reflect.getDefaultParameterValues on a function or something similar. This is not really unexpected as other dynamically typed languages with default parameters like Python and Ruby and PHP can do this and so can statically typed ones like C# or Scala. It would certainly be unexpected to not be able to do this.Kriemhild
Sorry, I wasn't aware you were looking for a ES2016 contribution via this answer, I thought I should merely attempt solving your problem using code that can work right now.Trocki
Why are you so sure that there is no code that does this right now :) I would not be surprised if there is an obscure method that does this that I'm just not aware of :)Kriemhild
Because I'm pretty confident of my JS skeelz :) But I'll be more than happy to learn something new and eat my hat doing so!Trocki
U
4

Is there a way to get the default value of x here?

No, there is no built-in reflection function to do such things, and it is completely impossible anyway given how default parameters are designed in JavaScript.

Note that toString()ing the function would not work as it would break on defaults that are not constant.

Indeed. Your only way to find out is to call the function, as the default values can depend on the call. Just think of

function(x, y=x) { … }

and try to get sensible representation for ys default value.

In other languages you are able to access default values either because they are constant (evaluated during the definition) or their reflection allows you to break down expressions and evaluate them in the context they were defined in.

In contrast, JS does evaluate parameter initializer expressions on every call of the function (if required) - have a look at How does this work in default parameters? for details. And these expressions are, as if they were part of the functions body, not accessible programmatically, just as any values they are refering to are hidden in the private closure scope of the function.
If you have a reflection API that allows access to closure values (like the engine's debugging API), then you could also access default values.

It's quite impossible to distinguish function(x, y=x) {…} from function(x) { var y=x; …} by their public properties, or by their behaviour, the only way is .toString(). And you don't want to rely on that usually.

Unyoke answered 11/10, 2015 at 12:56 Comment(2)
" it is completely impossible anyway given how default parameters are designed in JavaScript." - well that part is wrong. Your Reflect.getDefaultParameterValues could return getters for the parameters.Kriemhild
You mean it should return an object that upon evaluation executes side effects (the default initializer expression) and then throws an error like "x is not defined"? This is similarly impossible as getting closed-over values from a closure function object - it is thinkable, but not designed to be.Unyoke
L
1

I'd tackle it by extracting the parameters from a string version of the function:

// making x=3 into {x: 3}
function serialize(args) {
  var obj = {};
  var noWhiteSpace = new RegExp(" ", "g");
  args = args.split(",");
  args.forEach(function(arg) {
    arg = arg.split("=");
    var key = arg[0].replace(noWhiteSpace, "");
    obj[key] = arg[1];
  });
  return obj;
  }

 function foo(x=5, y=7, z='foo') {}

// converting the function into a string
var fn = foo.toString();

// magic regex to extract the arguments 
var args = /\(\s*([^)]+?)\s*\)/.exec(fn);

//getting the object
var argMap = serialize(args[1]); //  {x: "5", y: "7", z: "'foo'"}

argument extraction method was taken from here: Regular Expression to get parameter list from function definition

cheers!

PS. as you can see, it casts integers into strings, which can be annoying at times. just make sure you know the input type beforehand or make sure it won't matter.

Lording answered 4/10, 2015 at 15:1 Comment(8)
This does not work if the function is bound to anything but a literal. For example let y = 5; function foo(x=y) {};. This is unlike the Python Ruby or C# solutions.Kriemhild
@BenjaminGruenbaum This won't work in C# either. it has to be compile time constant.i.imgur.com/WWDwPnC.pngCycle
depending on your situation, you can tap that by using something like obj[key] = eval(arg[1]) || arg[1]; in the serialization fn. yeah, eval.. but that's actually a reasonable solutionLording
@RoyiNamir it's completely valid JavaScript though. It compiles and the default parameter works - but the code in this answer does not return the correct default value.Kriemhild
@Lording that would fail for cases where the lexical environment isn't the same. Not to mention this. I was hoping for more of a Reflect.getDefaultArguments or something like that, we've had some discussion of edge cases starting here: goo.gl/iqx5yfKriemhild
I was referring to let y = 5; function foo(x=y) {}; This is unlike the Python Ruby or C# solutions. In C# declaring default value to another variable is an error.Cycle
@RoyiNamir no need to be a nitpick, const int y = 6; int Bar(int x = y){ return x;} runs just fine in LINQPad and reflection gets out the correct values.Kriemhild
@BenjaminGruenbaum I'm not.Please be precise on what you write examples about and about analogy to C#. I Just tested the code and if that wasn't a CONST - it wouldn't compile . So it can not be a dynamic variable. in your example - let can have dynamic/calculated value. This is a huge difference.Cycle
S
1

As the question states, using toString is a limited solution. It will yield a result that could be anything from a value literal to a method call. However, that is the case with the language itself - it allows such declarations. Converting anything that's not a literal value to a value is a heuristic guess at best. Consider the following 2 code fragments:

let def;
function withDef(v = def) {
  console.log(v);
}

getDefaultValues(withDef); // undefined, or unknown?

def = prompt('Default value');
withDef();
function wrap() {
  return (new Function(prompt('Function body')))();
  // mutate some other value(s) --> side effects
}

function withDef(v = wrap()) {
  console.log(v);
}
withDef();
getDefaultValues(withDef); // unknown?

While the first example could be evaluated (recursively if necessary) to extract undefined and later to any other value, the second is truly undefined as the default value is non-determinitic. Of course you could replace prompt() with any other external input / random generator.

So the best answer is the one you already have. Use toString and, if you want, eval() what you extract - but it will have side effects.

Scary answered 7/10, 2015 at 6:58 Comment(0)
H
0

So, I've seen several other solutions to this question and also on these:

And other methods, besides the ones that rely on external dependencies and parsers, when tested with the following functions break, or return a messy result.

Test cases:

// 1
const testFn1 = (
    nonEmpty=' ',
    end=')', 
    y=end, 
    x, 
    z=x, 
    someCalc = 2*2.3-(2/2),
    comment='//', 
    template='`,', 
    {destructuring, test = '2', ...rest}, 
    {defDestruct, defDestruct_a, defDestruct_b} = {defDestruct: '1', defDestruct_a: 2, defDestruct_b: '3'},
    a = {1: '2', 'hi': [3,4,'==)']},
    anArr = [z,y],
    brackets = (1.23+')'),
    math = Math.round(1.34*10),
    h={a:'ok \"', b:{c:'now=2,tricky=2', d:'rlly'}},
    [test2, destructuring2 = 3, two],
    [destructuring3,, 
        notIgnored],
    [destructuring4, ...rest2],
    [defDestruct2, defDestruct2_a] = [4,5],
    {a: destrct5, c: destrct5_a, e : destrct5_b = 5},
    lastArg
) => {}

//2
const testFn2 = (a, 
    b=2, c={a:2, b:'lol'}, dee, e=[2,3,"a"],/*lol joke=2,2*/ foook=e, g=`test, ok`, h={a:'ok \"', b:{c:'lol=2,tricky=2', d:'lol'}}, i=[2,5,['x']], lastArg = "done" //ajaim, noMatch = 2)
) => {}

 
These are probably unusual cases, but I wanted to propose a stand-alone solution, that covers more edge cases and also returns an object with arguments as keys paired with their default values (evaluated not strings).

const delimiters = {
    brackets: {
        open: ["{","[","("],
        close: ["}","]",")"]
    },
    str: ["'",'"',"`"],
    destructuring: {
        obj: ["{","}"],
        arr: ["[","]"]
    },
    cmmt: {
        open: ["/*","//"],
        close: ["*/", null]
    }
}
/**
 * Parses the parameters of a function.toString() and returns them as an object
 * @param {*} fn The function to get the parameters from
 * @param {*} args arguments to pass to the function
 * @returns An object with the parameters and their default values
 * @author [bye-csavier](https://github.com/bye-csavier)
*/
function parseFnParams(fn, ...args){

    if(typeof fn != 'function') return null;

    let [subStr, parsedArgs, openDelimiters, argList] = ['', "", ['('], ''];
    
    fn = fn.toString().split('') // function to "char" array
    let len = fn.length, READ_ARG = true, INSIDE_STR = false, INSIDE_DESTRUCT = {obj: false, arr: false}, INSIDE_CMMT = {block: false, line: false};
    
    const addArg = () =>{
        subStr = subStr.replace(/\s|\./g, '');
        if(subStr.length == 0) return;
        parsedArgs += `${subStr}:${subStr},\n`;
        subStr = '';
    }

    for(let i=fn.indexOf('(')+1, char; i<len && openDelimiters.length > 0; i++){
        
        char = fn[i];
        let idx = -1;

        if(!INSIDE_STR && !INSIDE_CMMT.block && !INSIDE_CMMT.line){

            if(char == "/" && fn[i+1] == "/") INSIDE_CMMT.line = true;
            else if(char == "/" && fn[i+1] == "*") INSIDE_CMMT.block = true;
            else if(READ_ARG && char == delimiters.destructuring.obj[0]){ INSIDE_DESTRUCT.obj = true; openDelimiters.push(char); idx=0; }
            else if(READ_ARG && char == delimiters.destructuring.arr[0]){ INSIDE_DESTRUCT.arr = true; openDelimiters.push(char); idx=0; }
            else if((INSIDE_DESTRUCT.obj && char == delimiters.destructuring.obj[1]) || (INSIDE_DESTRUCT.arr && char == delimiters.destructuring.arr[1])){
                INSIDE_DESTRUCT.obj = false; INSIDE_DESTRUCT.arr = false;
                if(READ_ARG) addArg();
                openDelimiters.pop(); idx=0;
            }
            else if(delimiters.brackets.open.indexOf(char) > -1) 
            {
                openDelimiters.push(char);
                idx = 0;
            }
            else{
                idx = delimiters.brackets.close.indexOf(char);
                if(idx > -1 && delimiters.brackets.open.indexOf(openDelimiters[openDelimiters.length-1]) == idx) openDelimiters.pop();
            }

        }

        if(INSIDE_CMMT.line){ if(char == "\n") INSIDE_CMMT.line = false; }
        else if(INSIDE_CMMT.block){ if(char == "/" && fn[i-1] == "*") INSIDE_CMMT.block = false; }
        else if(READ_ARG && !INSIDE_STR && idx < 0){
            if(char == ',') addArg();
            else if(char == "="){addArg(); READ_ARG = false;}
            else if(INSIDE_DESTRUCT.obj && char == ':') subStr = "";
            else subStr += char;
        }
        else if((openDelimiters.length == 1 || (INSIDE_DESTRUCT.obj || INSIDE_DESTRUCT.arr)) && char == ',') READ_ARG = true;
        else if(delimiters.str.indexOf(char) > -1){
            if(!INSIDE_STR) INSIDE_STR = char;
            else if(INSIDE_STR == char) INSIDE_STR = false;
        }

        argList += char;
    }
    addArg(); // to also push the last arg

    fn = eval(`(${argList}=>{return {${parsedArgs}}}`);
    parsedArgs = fn(...args);

    return parsedArgs;
}

 
The function is nothing fancy or elegant, and neither is it fast. It does around 12 Ops/sec for the 1° test and 60 Ops/sec for the 2° test. But it's more robust and actually returns the function arguments in an organized way. In the current state, it should be used to precompute the default value of the parameters.

 
To summarize, this function should handle:

  • comments
  • non-constant defaults like (x,y=x)=>{}
  • whitespace
  • passing a list of arguments
  • anonymous functions
  • arrow functions

 
The support for destructuring assignment is limited. It supports only simple desturing assignment like [x,,y=2], {x, y=2}, {a: x, b: y=2}, [x, ...rest], and {x, ...rest}. It doesn't support (meaning that it breaks with): further destructuring like {a, b: {c:d}}; binding pattern as the rest property (e.g [a, b, ...{ length }] or [a, b, ...[c, d]]);

And it also assumes parameters to be wrapped inside round brackets, otherwise it will not work.  

 
Thanks to @Bergi for pointing out the issues in my previous solutions. As @Bergi (and others) said, using an already existing JS parser would be a more reliable solution.

Lastly, I don't know anything about parsing, so any improvement is welcomed.

Hwahwan answered 28/5 at 17:22 Comment(6)
Instead of writing more and more complicated regular expressions (and even a simple parenthesis counter), you may as well jump straight ahead into calling a real JavaScript parser.Unyoke
Thanks for the comment. I don't know alot about parsing, and even less about js parsers. I just wanted to create a standalone function only for this purpose. Wouldn't a parser be overkill?Hwahwan
No, it wouldn't be overkill, it just would properly solve the task without any edge cases left.Unyoke
Definitely, but I still think this method serves its purpose. I've just remembered the topic of non-costant defaults you brought up in your old post, I'd try to implement that too. Maybe I can rely on eval even more, but I've seen things can get complex. Besides that, I can't think of an uncovered edge case right now. But I'd like to fix them if there are.Hwahwan
Using eval(`((${argList})=>{return ${parsedArgs}})();`) is a nice idea (though Function(…)() would've been even better), but your string processing is lacking. Just demonstrating the bugs from the first few lines: getFnArgs((nonEmpty=' ', end=')', comment='//', template='`', {destructuring}) => {}). Solving all these edge cases amounts to writing a javascript parser, and it would be easier to use an existing library for that.Unyoke
I see, thanks for your comments! I'll add this premise to the post.Hwahwan

© 2022 - 2024 — McMap. All rights reserved.