Intro
So, I've seen several solutions regarding these questions:
And other standalone[1] methods, when tested with particular functions declarations, don't work or return a messy result.
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).
Solution
/**
* Parses the parameters of a function.toString() and returns them as an object
* @param {Function} fn The function to get the parameters from
* @param {*} ...args arguments to pass to the function
* @returns {Object} An object with the parameters and their default values
*/
function fnParams(fn, ...args){
if(typeof fn != 'function') return null;
fn = fn.toString()
let param = '',
paramsList = '(', // We init the param declaration, the closing ) is added during the parsing
paramsOBJ = '',
comment = {block:false,line:false},
ignoreUntil = null,
readP = true,
i=0,
ch,
whitespace = new RegExp('\\s');
const LEN = fn.length;
for(i=0;i<LEN;++i){
ch = fn[i];
if(comment.line){ if(ch=="\n") comment.line = false; }
else if(comment.block){ if(ch=="/" && fn[i-1]=="*") comment.block = false; }
else if(ch == "/" && fn[i+1] == "/") comment.line = true;
else if(ch == "/" && fn[i+1] == "*") comment.block = true;
else if(ch == "("){i++; break}
}
if(i==LEN) return null;
while(i<LEN){
ch = fn[i];
paramsList += ch;
if(ignoreUntil){if(ch==ignoreUntil) ignoreUntil=null;}
else if(comment.line){ if(ch=="\n") comment.line = false; }
else if(comment.block){ if(ch=="/" && fn[i-1]=="*") comment.block = false; }
else if(ch == "/" && fn[i+1] == "/") comment.line = true;
else if(ch == "/" && fn[i+1] == "*") comment.block = true;
else if(readP){
if(ch=="," || ch=="=" || ch==")"){
if(param!='') paramsOBJ += param+',';
param='';
if(ch=="=") readP = false
else if(ch==")") break;
}
else if(ch==":") param = '';
else if(!( ch=='.' ||
whitespace.test(ch) ||
ch=="{" ||
ch=="[" ||
ch=="]" ||
ch=="}" )) param+=ch;
}
else{
if(ch==",") readP = true;
else if(ch==")") break;
else if(ch=='"') ignoreUntil = '"';
else if(ch=="'") ignoreUntil = "'";
else if(ch=='`') ignoreUntil = '`';
else if(ch=='{') ignoreUntil = '}';
else if(ch=='[') ignoreUntil = ']';
else if(ch=='(') ignoreUntil = ')';
}
i++;
}
return eval(`${paramsList}=>{return {${paramsOBJ}}}`).apply(this,args);
}
The solution is nothing fancy or elegant. The avarage execution time is 0.1ms ± 4%.
IMO, it's a more robust solution, and actually returns the function arguments in an organized way.
In the current state, it could be used to precompute the parameters.
To summarize, this solution handles everything besides functions like x=>{y} and x=>y.
Tests
let tests = [
{
name: "Default parameters | Basic",
fn(x=1,y=2,z=3){
return{
x,
y,
z
}
},
args:[,"PASSED",],
this:this
},
{
name: "Custom this",
fn(
a = this,
b = this.special
){
return{
a,
b
}
},
args:[],
this:{special:":P"}
},
{
name: "Rest parameters",
fn(a,b=2, ...andTheRest){
return{
a,
b,
andTheRest
}
},
args:[1,,3,4,5],
this:this
},
{
name: "Default parameters | Complex",
fn(
a,
b = 5+1.2-(4/2),
c = this,
d = Math.round(1.34*10),
e = d,
f = arguments,
g = this.special,
h = [a,b,1],
i = {a:b,b:a}
){
return{
a,
b,
c,
d,
e,
f,
g,
h,
i
}
},
args:[],
this:{special:":P"}
},
{
name: "Tricky comment | Block outside ",
fn /*(tricked=1)*/(a=1) {
return {
a
};
},
args:[],
this:this
},
{
name: "Tricky comment | Line outside ",
fn //(tricked=1)
(a=1) {
return {
a
};
},
args:[],
this:this
},
{
name: "Tricky comment | Block inside ",
fn (a=1,/*tricked,*/b=2) {
return {
a,
b
};
},
args:[],
this:this
},
{
name: "Tricky comment | Line inside ",
fn (a=1,// tricked
b=2) {
return {
a,
b
};
},
args:[],
this:this
},
{
name: "Tricky strings",
fn (a="1,trickA",b='2,trickB',c=`3,trickC`) {
return {
a,
b,
c
};
},
args:[],
this:this
},
{
name: "Tricky delimiters",
fn ({a="}",b='}',c=`}`}={}, [d="]"]=[]) {
return {
a,
b,
c,
d
};
},
args:[],
this:this
},
{
name: "Destructuring | Basic",
fn([a,b,c],{d,e,f}){
return {
a,
b,
c,
d,
e,
f
}
},
args:[[1,2,3],{f:"<",e:"=",d:"3"}],
this:this
},
{
name: "Destructuring | Renaming",
fn({d:rD,e:rE,f:rF}){
return {
rD,
rE,
rF
}
},
args:[{f:"<",e:"=",d:"3"}],
this:this
},
{
name: "Destructuring | Further",
fn(
{a, b: {c: d} }
){
return {
a,
d
}
},
args:[{ a: 1, b: { c: 2 } }],
this:this
},
{
name: "Destructuring | Default",
fn(
[a = 1] = [],
{ b = 2 } = { b: undefined },
{ c = 3 } = { c: null },
{ d = 4 } = {},
){
return {
a,
b,
c,
d
}
},
args:[],
this:this
},
{
name: "Destructuring | Nested",
fn(
[a = 1, [b = 2, c = 3, [, d]]] = [, [, -1, [, -2]]],
{ e = 1, f: g = 2, h: i , nA:{k,l,nB:{m:n,o:p=4}}} = { e: -1, h:-2, nA:{l:-3,nB:{m:-4}} },
) {
return {
a,
b,
c,
d,
e,
g,
i,
k,
l,
n,
p
};
},
args:[],
this:this
},
{
name: "Destructuring | Binding Rest",
fn(
[a, b, ...{ length }] = [1, 2, 3],
[c, d, ...[e, f, ...[g, h]]] = [1, 2, 3, 4, 5, 6],
) {
return {
a,
b,
length,
c,
d,
e,
f,
g,
h,
};
},
args:[],
this:this
},
{
name: "Destructuring | Rest",
fn(
{ a, ...b } = { a: 1, b: 2, c: 3 },
[c, ...d] = [1, 2, 3, 4, 5, 6],
) {
return {
a,
b,
c,
d
};
},
args:[],
this:this
},
{
name: "Destructuring | Mixed",
fn(
{b:a, c:[b,c=1,{d}]} = {a:-1,b:-2,c:[-3,,{d:-4,e:-5}]}
) {
return {
a,
b,
c,
d,
};
},
args:[],
this:this
},
{
name: "Destructuring | Computed Property",
fn({ ["a"]: aA, ["b"]: bB = 1 , c:{['c']:cC}} = { ["a"]: -1 , c:{['c']:-2}}) {
return {
aA,
bB,
cC
};
},
args:[],
this:this
},
{
name: "Destructuring | Nested + Default",
fn(
{ a, b:{c}={c:-2}, d:[e]=[-3]} = {a:-1}
) {
return {
a,
c,
e
};
},
args:[],
this:this
},
{
name: "Destructuring | Alternative identifier",
fn(
{ a, b:{c}={c:-2}, d:[e]=[-3]} = {a:-1}
) {
return {
a,
c,
e
};
},
args:[],
this:this
},
]
Conclusion
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.
This solution is here to offer a standalone approach.
Lastly, I don't know much about parsing, so any improvement is welcomed.
Notes
- "standalone": By standalone here I mean: methods that don't rely on JS-Parsers (or other dependencies) to get the job done.