
本文深入探讨了如何在javascript中构建一个高阶函数,使其能够为目标函数灵活地设置默认参数,并支持多次链式调用。针对在处理已装饰函数时,`func.tostring()` 方法无法正确解析原始参数签名的问题,文章详细阐述了如何利用 `weakmap` 结合闭包来维护函数原始参数签名的有效解决方案,从而确保默认参数设置的准确性和可扩展性。
在JavaScript中,高阶函数(Higher-Order Function)是一种常见的编程范式,它能够接收函数作为参数,或将函数作为返回值。一个典型的应用场景是为现有函数提供额外的功能,例如设置默认参数。我们希望创建一个名为 defaultMethod 的高阶函数,它接收一个目标函数和一个包含默认参数的键值对对象,然后返回一个新的函数。当新函数被调用时,如果某些参数未提供或为 undefined,则使用预设的默认值。更进一步的需求是,这个 defaultMethod 应该支持链式调用,即可以多次为同一个(或已装饰的)函数设置或更新默认参数。
最初的实现尝试通过解析函数的 toString() 结果来获取其参数名:
function defaultMethod(func, params) {
var funcStr = func.toString();
let requiredArgs = funcStr
.slice(funcStr.indexOf('(') + 1, funcStr.indexOf(')')) // 获取括号内的内容
.match(/([^\s,]+)/g) || []; // 匹配参数名,例如 ['a', 'b']
return function (...args) {
let calledArgs = args;
// 填充默认参数逻辑
for (let i = calledArgs.length; i < requiredArgs.length; i++) {
if (calledArgs[i] === undefined) {
calledArgs[i] = params[requiredArgs[i]];
}
}
return func(...calledArgs);
};
}这种方法对于首次调用 defaultMethod 并传入原始函数(如 function add(a,b) { return a+b; })时工作良好。此时 func.toString() 会返回如 "function add(a,b) { return a+b; }" 这样的字符串,可以正确解析出 ['a', 'b']。
然而,当 defaultMethod 被第二次调用,并且传入的 func 参数是之前由 defaultMethod 返回的装饰器函数时,问题就出现了。例如:
立即学习“Java免费学习笔记(深入)”;
var add_ = defaultMethod(add, {b:9}); // 第一次调用
add_ = defaultMethod(add_, {b:3, a:2}); // 第二次调用,此时func是add_此时,add_ 是一个由 defaultMethod 创建的匿名函数,其 toString() 结果通常是 function (...args) { ... }。这意味着 func.toString() 将不再提供原始函数的参数签名(如 a, b),而是返回 (...args),导致 requiredArgs 被错误地解析为 ['...args'] 或空数组,从而破坏了默认参数的正确匹配逻辑。
为了解决 func.toString() 的局限性,我们需要一种机制来持久化存储每个装饰器函数与其对应的原始参数签名。WeakMap 是一个理想的选择,因为它允许我们将对象(在这里是装饰器函数)作为键,并将任意值(在这里是参数名数组)作为值。WeakMap 的一个关键特性是它的键是弱引用的,这意味着如果键对象没有其他引用,它就会被垃圾回收,避免内存泄漏。
我们可以通过一个闭包来封装 WeakMap,使其成为 defaultMethod 函数的私有状态,从而避免污染全局作用域。
以下是使用 WeakMap 改进后的 defaultMethod 实现:
function add(a, b) {
return a + b;
}
// 使用闭包来创建私有的 registry
const defaultMethod = (function () {
const registry = new WeakMap(); // WeakMap 用于存储装饰器函数及其对应的参数名
return function (func, params) {
// 尝试从 registry 中获取当前 func 的参数名
// 如果 func 是一个已经被 defaultMethod 装饰过的函数,
// 那么它的参数名应该已经存储在 registry 中
let requiredArgs = registry.get(func);
// 如果 registry 中没有,说明 func 是一个原始函数,或者是一个新的未被注册的函数
if (!requiredArgs) {
const funcStr = func.toString();
// 从 func.toString() 中解析参数名
requiredArgs = funcStr
.slice(funcStr.indexOf('(') + 1, funcStr.indexOf(')'))
.match(/([^\s,]+)/g) || [];
}
console.log("当前函数参数名为:", ...requiredArgs); // 辅助调试
// 创建新的装饰器函数
const decoratedFunc = function (...args) {
let calledArgs = [...args]; // 复制一份传入的参数,避免直接修改 arguments 对象
// 遍历所需参数,如果传入参数不足且对应位置为 undefined,则应用默认值
for (let i = 0; i < requiredArgs.length; i++) {
if (calledArgs[i] === undefined) {
// 注意:这里需要确保 params[requiredArgs[i]] 能够正确获取到值
// 并且不会覆盖已经传入的 undefined 值(如果用户确实想传入 undefined)
// 原始逻辑是 `if (calledArgs[i] === undefined)`,所以如果用户明确传入了 undefined,
// 也会被默认值覆盖。这符合原始问题的意图。
calledArgs[i] = params[requiredArgs[i]];
}
}
// 调用原始函数或上一个装饰器函数
return func(...calledArgs);
};
// 将新创建的装饰器函数及其对应的参数名注册到 WeakMap 中
registry.set(decoratedFunc, requiredArgs);
return decoratedFunc;
};
})();让我们通过具体的测试用例来验证这个改进后的 defaultMethod 的行为。
// 原始函数
function add(a, b) {
return a + b;
}
console.log("--- 第一次设置默认值:b=9 ---");
let add_ = defaultMethod(add, { b: 9 });
// 测试用例 1: 只传入 a 的值
console.log("调用 add_(10): 预期 19 (10 + 9)");
console.log("结果:", add_(10)); // 输出 19 (10 + 9)
// 测试用例 2: 传入 a 和 b 的值
console.log("调用 add_(10, 7): 预期 17 (10 + 7)");
console.log("结果:", add_(10, 7)); // 输出 17 (10 + 7)
// 测试用例 3: 不传入任何值
console.log("调用 add_(): 预期 NaN (undefined + 9)");
console.log("结果:", add_()); // 输出 NaN (因为 a 为 undefined)
console.log("\n--- 第二次设置默认值:b=3, a=2 (链式调用) ---");
// 此时 add_ 是一个装饰器函数,defaultMethod 会从 registry 中获取其参数签名
add_ = defaultMethod(add_, { b: 3, a: 2 });
// 测试用例 4: 只传入 a 的值
console.log("调用 add_(10): 预期 13 (10 + 3)");
console.log("结果:", add_(10)); // 输出 13 (a=10, b取新默认值3)
// 测试用例 5: 不传入任何值
console.log("调用 add_(): 预期 5 (2 + 3)");
console.log("结果:", add_()); // 输出 5 (a取默认值2, b取默认值3)
console.log("\n--- 第三次设置默认值:c=3 (无关参数) ---");
// 传入一个与原始函数参数不匹配的默认值
add_ = defaultMethod(add_, { c: 3 });
// 测试用例 6: 只传入 a 的值
console.log("调用 add_(10): 预期 13 (10 + 3)");
console.log("结果:", add_(10)); // 输出 13 (a=10, b取默认值3,c不影响)通过上述示例,我们可以看到,即使 defaultMethod 被多次链式调用,并且每次都传入前一个装饰器函数,它依然能够正确地识别原始函数的参数签名并应用新的默认值。
通过结合 WeakMap 和闭包,我们成功地创建了一个健壮且支持链式调用的高阶函数 defaultMethod,它能够为任意函数灵活地设置和更新默认参数,有效地解决了 func.toString() 在复杂场景下的局限性。
以上就是JavaScript 高阶函数:实现可链式调用的默认参数设置的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号