【问题标题】:When can we pass an unwrapped function as a parameter我们什么时候可以将展开的函数作为参数传递
【发布时间】:2020-12-27 16:23:22
【问题描述】:

在 Javascript 和许多其他编程语言中,我们可以将函数作为参数传递给其他函数。这是函数式编程中的常见做法。

我们知道需要一个包装器来在那里放置一个断点、查看它在堆栈跟踪中的位置、更好地控制参数或在调用之前/之后添加逻辑。

是否还有其他客观原因不将展开的函数作为参数传递?

myFunction1(x => myFunction2(x)) // wrapped
myFunction1(myFunction2) // unwrapped

【问题讨论】:

  • 当然,当您想放置断点时,请继续使用包装器...如果您需要用于removeEventListener 的函数引用,那么您必须分配您的包装器函数到变量或命名它。不确定您的问题是什么。
  • 请注意e => myFunction(e) 等同于myFunction。但是,e => myFunction([]) (e) 不等同于 myFunction([]),前提是您对数组进行了变异。
  • @trincot 我想继续使用包装器是明智的。不相信(还?)直接传递函数的想法。
  • 真的取决于你的需要。如果您不需要做任何其他事情(例如调用函数的 before 断点,或者传递一个额外的参数,...)并且您不需要特定的 this 绑定,那么传递在我看来,函数引用看起来更好,并且您将少一个包装级别。但同样,这种选择实际上是由您的需求驱动的。

标签: javascript functional-programming ramda.js


【解决方案1】:

询问“是否应该”使用一种或另一种技术在 SO 中大多是禁区。

不过,我将回答一个相关问题,即您何时可以使用该技术,何时不能使用该技术,以及可能有哪些(不利)优势。

你总是可以写foo (x => bar (x))。你不能总是写foo (bar)。为什么可以在another Q & A 中找到一个示例。根据我的经验,一个常见的例子是带有第二个参数的递归函数,该参数在初始调用中默认并在后续调用中传递。这样的函数不能通过简单的引用map来成功传递,因为map提供了除了期望值之外的额外参数。

但这是不寻常的情况。如果您的函数没有默认参数,则包装器似乎没有什么用处。一种说法是,如果你要用foo (x => bar (x)) 替换foo (bar),为什么不更进一步,使用foo (y => (x => bar (x)) (y))foo (z => (y => (x => bar (x)) (y)) (z))

包装器工作,但没有添加任何东西......除了你指出的悬挂断点的地方。

这对您来说可能是一个合理的案例。这些天我没有花太多时间在调试器上,但是当我这样做时,我可能偶尔会临时添加这样的包装器。但我后来删除了它们。我发现干净的代码非常重要,不必要的包装器只会使事情变得混乱。在代码审查中,我经常反对这种模式:

const foo = (...args) => {
    // do something with args
    return new Promise ((resolve, reject) => {
        bar (something).then(
            (a) => {
                resolve (a);
             }, (err) => {
                reject (err);
             }
        );
    });
}

我总是需要指出的可以更简洁地写成:

const foo = (...args) => {
    // do something with args
    return new Promise ((resolve, reject) => {
        bar (something) .then (resolve, reject);
    });
}

那么我必须进一步指出,即使这样也可以更好地写成

const foo = (...args) => {
    // do something with args
    return bar (something);
}

关键是resolvereject 周围的函数包装很混乱。 bar 周围的 Promise 包装器也是如此。它不会影响结果,对性能的影响很小,但它几乎无法平衡混乱。

(你看,我在这里试图避免意见,但真的不能。)

【讨论】:

  • 感谢您的回答。不要开始另一场辩论,但我会尽可能使用 async/await。然后 eslint 规则可以帮助避免混乱,即 no-return-await 或 @typescript-eslint/no-unnecessary-condition...
  • @Rivenfall:我对async/awaitPromise 有不同的看法。但是在培训中,我总是想确保用户在我提供async 之前了解Promises 是什么以及他们是如何工作的。我的部分问题是我don't really like Promises,更喜欢未来/任务实现。
【解决方案2】:
call(e => foo(e))

完全一样:

call(foo)

只有一层不必要的间接;)

这与函数式编程或柯里化无关,我看不出它会如何影响您的调试体验。

当您想要锁定某些参数或签名不兼容时,您通常会换行。例如,

call(a => foo(5, a))

但在这种情况下,您可能需要考虑使用柯里化。

【讨论】:

  • 我不认为包装函数会增加复杂性,因为使用函数名和左括号很容易搜索代码库以查找调用者。它确实有助于调试体验,因为您可以放置​​断点并在调用堆栈中多留一个位置。直接传递函数似乎与一般工具(包括搜索)不太兼容,并且似乎更容易推理。
  • @Rivenfall 我从来没有说过它增加了复杂性,只是恕我直言,这根本是不必要的。至于没有它们的调试体验,我的表现也是如此?‍♂️。
【解决方案3】:

您无需使用匿名函数包装函数即可将其指定为事件处理程序。如果函数应该获取事件对象,这很好。

window.addEventListener('click', myFunction)

此外,使用匿名函数包装函数会阻止您删除事件处理程序,因为 remove 需要添加的相同事件处理程序。

这将起作用:

window. removeEventListener('click', myFunction)

这不会:

window.removeEventListener('click', e => myFunction(e))

如果您需要向事件处理程序传递更多参数,则需要对其进行柯里化,并将返回的函数分配给一个变量,以便您可以删除处理程序:

const createEventHandler = param => e => {}

const eventHandler = createEventHandler(true)

window.addEventListener('click', eventHandler)
// ...
window.removeEventListener('click', eventHandler)

【讨论】:

  • 事件处理程序依赖函数对象标识是 JS/DOM 最引人注目的缺陷之一。
  • 确实如此。 jQuery 让生活变得更轻松,并且在大多数现代框架中添加和删除事件都是以声明方式完成的,并且框架处理 JS 端。
  • 我喜欢原生,因为它是原生的。 EventTarget API 尽可能简单。我想我不应该在函数式编程问题中讨论事件,因为它可以说是面向对象的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-10-24
  • 2011-09-20
  • 2016-05-04
  • 2019-05-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多