有没有办法在 JavaScript 中检索函数的默认参数值?
function foo(x = 5) {
// things I do not control
}
有没有办法在这里获取
x
的默认值?最好是这样的:
getDefaultValues(foo); // {"x": 5}
请注意,
toString
该函数将不起作用,因为它会在默认值不恒定时中断。
由于我们在 JS 中没有经典的反射(如 C#、Ruby 等),因此我们必须依靠我最喜欢的工具之一,正则表达式来为我们完成这项工作:
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" }
如果您要使用此功能,只需确保缓存正则表达式以提高性能即可。
感谢 bubersson 对于前两个正则表达式的提示
有没有办法在这里获取
的默认值?x
不,没有内置的反射函数可以做这样的事情,而且考虑到 JavaScript 中默认参数的设计方式,这是完全不可能的。
请注意,
该函数将不起作用,因为它会在默认值不恒定时中断。toString()
确实如此。找出答案的唯一方法是调用该函数,因为默认值可能取决于调用。只要想想
function(x, y=x) { … }
并尝试获得
y
默认值的合理表示。
在其他语言中,您可以访问默认值,因为它们是常量(在定义期间评估),或者它们的反射允许您分解表达式并在定义它们的上下文中评估它们。
相比之下,JS 会在每次调用函数时评估参数初始值设定项表达式(如果需要) - 请查看
this
在默认参数中如何工作? 了解详细信息。这些表达式就像函数体的一部分一样,无法以编程方式访问,就像它们引用的任何值都隐藏在函数的私有闭包范围中一样。通过公共属性或行为来区分
function(x, y=x) {…}
和 function(x) { var y=x; …}
是完全不可能的,唯一的方法就是 .toString()
。而且你通常不想依赖它。
我会通过从函数的字符串版本中提取参数来解决这个问题:
// 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'"}
参数提取方法取自这里:从函数定义中获取参数列表的正则表达式
干杯!
PS。正如您所看到的,它将整数转换为字符串,这有时会很烦人。只需确保您事先知道输入类型或确保它无关紧要。
正如问题所述,使用
toString
是一种有限的解决方案。它将产生一个结果,可以是从值文字到方法调用的任何结果。然而,语言本身就是这种情况——它允许这样的声明。将任何非字面值转换为值充其量只是一种启发式猜测。考虑以下 2 个代码片段:
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?
虽然可以评估第一个示例(如果需要,可以递归地)以提取
undefined
并随后提取任何其他值,但第二个示例实际上是未定义的,因为默认值是不确定的。当然,您可以用任何其他外部输入/随机生成器替换 prompt()
。
所以最好的答案就是你已经有了的答案。使用
toString
,如果需要,可以使用 eval()
提取的内容 - 但它会有副作用。
所以,我已经看到了关于这个问题的其他几个解决方案,还有这些:
但是当使用以下边缘情况测试它们时,它们会崩溃,或者返回混乱的结果。
边缘情况:
function test(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']] //ajaim
) {
return {
a: a,
b: b,
c: c,
dee: dee,
e: e,
foook: foook,
g: g,
h: h,
i: i
}
}
这显然是一种不太可能发生的情况,但我想提出自己的解决方案,它涵盖了更多边缘情况,并且还返回一个对象,该对象的参数作为与默认值配对的键。
/**
* Parses the parameters of a function.toString() and returns them as an object
* @param {*} fn The function to get the parameters from
* @param {*} asArray If true, it returns an array instead of an object
* @returns An object (or array) with the parameters and their default values
* @author [bye-csavier](https://github.com/bye-csavier)
*/
function getFnArgs(fn, asArray = false){
if(!(typeof fn === 'function')) return null;
fn = fn.toString();
fn = fn.replace(/\s/gm, ''); //remove all white spaces
fn = fn.match(/(?<=\()(.+?)(?=\))/); //get the function arguments section
if(fn == null) return null; else fn = fn[0];
fn = fn.replace(/(\/\/.*$)|(\/\*[\s\S]*?\*\/)/g, '') //removes comments
fn = fn.replace(/(?<!\\)["']/g, '`') //simplifies string delimiters for less checks
let subStr = ''; let parsedArgs = "{"; let delimitersTest = {};
let openDelimiter = [];
const strToArg = (str) => {
parsedArgs += `'${str}':${str},`;
}
delimitersTest.open = "";
delimitersTest.close = "";
//recursive RegEx is not supported therefore this is the best I can do
fn.replace(/\{(.+?)\}|\[(.+?)\]|\((.+?)\)|`(.+?)`/g, (match, p1, p2, p3, p4) => {
if(p1){ delimitersTest.open += "{"; delimitersTest.close += "}"; }
else if(p2){ delimitersTest.open += "["; delimitersTest.close += "]"; }
else if(p3){ delimitersTest.open += "("; delimitersTest.close += ")"; }
else if(p4){ delimitersTest.open += "`"; delimitersTest.close += "`"; }
return match;
});
delimitersTest.isOpening = (char) => {
if(delimitersTest.open.indexOf(char) > -1){
openDelimiter.push(char);
}
}
delimitersTest.isClosing = (char) => {
let idx = delimitersTest.close.indexOf(char);
if(idx < 0) return;
if(delimitersTest.open.indexOf(openDelimiter[openDelimiter.length-1]) == idx){
openDelimiter.pop();
}
}
let argList = fn; //save a copy of the original argList for later evaluation
fn = (fn+',').split('') // string to array, and added trailing comma to push also the last argument
let len = fn.length, CAN_READ = true;
for(let i=0; i<len; i++){
if(fn[i] == '=') CAN_READ = false;
else if(openDelimiter.length > 0){
if(openDelimiter.length > 0) delimitersTest.isClosing(fn[i])
}
else if(openDelimiter.length == 0 && (fn[i] == ',')){
strToArg(subStr);
subStr = '';
CAN_READ = true;
}
else{
if(delimitersTest.isOpening(fn[i])) CAN_READ = false;
if(CAN_READ) subStr += fn[i];
}
}
parsedArgs += '}';
fn = `((${argList})=>{return ${parsedArgs}})();`;
parsedArgs = eval(fn);
if(!asArray) return parsedArgs;
return Object.values(parsedArgs);
}
这个函数没什么花哨,也不快,它的速度约为 50 Ops/sec,但它涵盖了边缘情况,并且实际上以有组织的方式返回函数参数。