JavaScript:递归匿名函数?

141

假设我有一个基本的递归函数:

function recur(data) {
    data = data+1;
    var nothing = function() {
        recur(data);
    }
    nothing();
}

如果我有一个匿名函数,我该怎么做呢?...

(function(data){
    data = data+1;
    var nothing = function() {
        //Something here that calls the function?
    }
    nothing();
})();

我希望有一种方法可以调用调用当前函数的函数... 我曾经在某个地方看到过脚本(我现在记不起来了)可以告诉您被调用的函数的名称,但我现在无法回想起任何与此相关的信息。


1
@thenduks:和使用匿名函数的原因一样。只是有时候需要使用递归。 - poke
5
很遗憾arguments.callee存在,而这个函数并没有做任何有用的事情。我正在查找Y组合子,该东西永远不会变得有用... - Kobi
1
是的,正如Kobi链接的那样,使用固定点组合器(例如Y)来进行匿名递归函数而无需arguments.callee。 - steamer25
1
请参考http://w3future.com/weblog/stories/2002/02/22/javascriptYCombinator.html,了解JS中Y组合子的示例。 - steamer25
这是由Y-Combinator在函数式语言中完成的。有关详细说明,请查看此链接:[https://dev59.com/qFoU5IYBdhLWcg3wzZA1#41412648]。 - Redu
21个回答

162

即使您将函数创建为值而不是“函数声明”语句,也可以为函数命名。换句话说:

可以给函数命名。

(function foo() { foo(); })();

这是一个递归函数,使用了栈溢出的技巧。但一般情况下 你可能不想 不建议使用它,因为在各种JavaScript实现中存在一些奇怪的问题。(注:这是一个相当古老的评论;部分/许多/全部Kangax的博客文章中描述的问题可能已经在更现代化的浏览器中得到解决。)

当你给函数起这样的名称时,名称在函数外部不可见(好吧,它本应该不可见;这是其中的一个奇怪之处)。这就像Lisp中的"letrec"。

至于arguments.callee,它在“严格”模式下被禁止,并且通常被认为是一个糟糕的事情,因为它使某些优化难以实现。它也比人们期望的要慢得多。

编辑 - 如果你想要一个可以调用自身的“匿名”函数的效果,你可以像这样做(假设你正在把函数作为回调传递或类似情况):

asyncThingWithCallback(params, (function() {
  function recursive() {
    if (timeToStop())
      return whatever();
    recursive(moreWork);
  }
  return recursive;
})());

这样做的作用是定义一个函数,使用良好、安全且在IE中不会出现错误的函数声明语句,创建一个本地函数,其名称不会污染全局命名空间。包装器(真正匿名)函数只返回该本地函数。


我猜用 (() => { call_recursively_self_here() })() 并递归调用函数自身是不可能的,对吗?我必须给它一个名字。 - Qwerty
1
@Qwerty 好的,你可以像我回答中的最后一个例子那样做。将箭头函数绑定到包装函数中的一个局部变量上,以便你的箭头函数可以使用变量名引用自身。然后,包装函数会返回该变量(它引用了箭头函数)。 - Pointy
在你的答案中,你的函数(recursive)通过名称调用自身,因此实际上它不是匿名递归函数。但是用户633183在这里展示了(https://dev59.com/dG865IYBdhLWcg3wTctY#43195580),这是可能的。 - Kamil Kiełczewski
@Pointy,但是OP问了这个问题。从理论上讲,这是一个有趣的问题。 - Kamil Kiełczewski
1
@Pointy 也许一些黑客会找到用途 ;) - Kamil Kiełczewski
显示剩余4条评论

37

在评论中人们谈论了Y组合器,但没有人将其写成答案。

Y组合器可以用以下javascript代码定义:(感谢steamer25提供的链接)

var Y = function (gen) {
  return (function(f) {
    return f(f);
  }(function(f) {
    return gen(function() {
      return f(f).apply(null, arguments);
    });
  }));
}

当您想传递匿名函数时:

(Y(function(recur) {
  return function(data) {
    data = data+1;
    var nothing = function() {
      recur(data);
    }
    nothing();
  }
})());

这个解决方案最重要的一点是,你不应该使用它。


20
为什么不能使用这个解决方案是最需要注意的事情。 - alexia
10
它不会很快。实际使用起来很丑(尽管在概念上很美!)。你避免了给函数一个标签或变量名的麻烦(我不明白为什么这会成为一个问题),但你仍然将其作为参数传递给 Y 的外部函数并给它命名。所以,通过所有这些麻烦,你并没有获得任何好处。 - zem
不要忘记提到这个函数不是堆栈安全的。循环几千次就会导致堆栈溢出。 - Mulan
嗨,我想提出一个稍微“更简洁”的修改,因为 .apply(null,arguments) 对我来说看起来很丑陋:var Y = function (gen) { return (function(f) { return f(f); }(function(f) { return gen(function(x) { return f(f)(x); }); })); } 或者等价于使用箭头符号(有效的 js 代码)(( function(x){return y} 等同于 (x => y) ):var Y = gen => ( f => f(f) ) ( f => gen ( x => f(f)(x) ) ) - bogec

36

U组合器

通过将一个函数作为参数传递给自身,函数可以使用其参数而不是名称来进行递归! 因此,给定给 U 的函数应至少有一个参数,该参数将绑定到该函数(它本身)。

在下面的示例中,我们没有退出条件,因此我们会无限循环,直到发生堆栈溢出。

const U = f => f (f) // call function f with itself as an argument

U (f => (console.log ('stack overflow imminent!'), U (f)))

我们可以使用各种技术来停止无限递归。在这里,我将编写我们的匿名函数,以返回等待输入的 另一个 匿名函数; 在本例中,是一些数字。当提供一个数时,如果它大于0,我们将继续递归,否则返回0。

const log = x => (console.log (x), x)

const U = f => f (f)

// when our function is applied to itself, we get the inner function back
U (f => x => x > 0 ? U (f) (log (x - 1)) : 0)
// returns: (x => x > 0 ? U (f) (log (x - 1)) : 0)
// where f is a reference to our outer function

// watch when we apply an argument to this function, eg 5
U (f => x => x > 0 ? U (f) (log (x - 1)) : 0) (5)
// 4 3 2 1 0

这里并不立即显而易见的是,当我们首次使用 U 组合子将函数应用于自身时,它会返回一个等待第一个输入的函数。如果我们给它一个名称,就可以有效地使用 lambda 表达式(匿名函数)构建递归函数。

const log = x => (console.log (x), x)

const U = f => f (f)

const countDown = U (f => x => x > 0 ? U (f) (log (x - 1)) : 0)

countDown (5)
// 4 3 2 1 0

countDown (3)
// 2 1 0

只是这不是直接递归——一个使用自己的名字调用自己的函数。我们定义的countDown函数在其主体内没有引用自身,但仍然可以进行递归。

// direct recursion references itself by name
const loop = (params) => {
  if (condition)
    return someValue
  else
    // loop references itself to recur...
    return <b>loop</b> (adjustedParams)
}

// U combinator does not need a named reference
// no reference to `countDown` inside countDown's definition
const countDown = U (f => x => x > 0 ? U (f) (log (x - 1)) : 0)

如何使用U组合子从现有函数中删除自引用

本文将向您展示如何将使用自引用的递归函数更改为使用U组合子替代自引用的函数。

const factorial = x =>
  x === 0 ? 1 : x * factorial (x - 1)
  
console.log (factorial (5)) // 120

现在使用U组合子来替换对factorial的内部引用

const U = f => f (f)

const factorial = U (f => x =>
  x === 0 ? 1 : x * U (f) (x - 1))

console.log (factorial (5)) // 120

基本的替换模式是这样的。请注意,我们将在下一部分中使用类似的策略。
// self reference recursion
const foo =         x => ...   foo (nextX) ...

// remove self reference with U combinator
const foo = U (f => x => ... U (f) (nextX) ...)

Y组合子

相关: 使用镜子类比讲解U和Y组合子

在前一节中,我们了解了如何将自我引用递归转换为不依赖命名函数的递归函数,方法是使用 U 组合子。但有一个小烦恼,就是必须记住总是将函数作为第一个参数传递给它本身。那么,Y 组合子建立在 U 组合子的基础上,并消除了那个繁琐的部分。这是个好事情,因为减少复杂性是我们构建函数的主要原因。

首先,让我们推导出自己的 Y 组合子。

// standard definition
const Y = f => f (Y (f))

// prevent immediate infinite recursion in applicative order language (JS)
const Y = f => f (<b>x =></b> Y (f) <b>(x)</b>)

// remove reference to self using U combinator
const Y = <b>U (h =></b> f => f (x => <b>U (h)</b> (f) (x))<b>)</b>

现在我们将看到它的使用与 U-combinator 的比较。请注意,为了进行递归,我们可以简单地调用 f() 而不是 U(f)

const U = f => f (f)

const Y = U (h => f => f (x => U (h) (f) (x)))

Y (f => (console.log ('stack overflow imminent!'),  f ()))

现在我将演示使用YcountDown程序 - 您会发现这两个程序几乎相同,但Y组合子使事情变得更加简洁。

const log = x => (console.log (x), x)

const U = f => f (f)

const Y = U (h => f => f (x => U (h) (f) (x)))

const countDown = Y (f => x => x > 0 ? f (log (x - 1)) : 0)

countDown (5)
// 4 3 2 1 0

countDown (3)
// 2 1 0

现在我们将看到 阶乘

const U = f => f (f)

const Y = U (h => f => f (x => U (h) (f) (x)))

const factorial = Y (f => x =>
  x === 0 ? 1 :  x * f (x - 1))

console.log (factorial (5)) // 120

正如您所看到的,f 成为了递归本身的机制。要进行递归,我们像调用普通函数一样调用它。我们可以使用不同的参数多次调用它,结果仍然是正确的。由于它是普通函数参数,因此我们可以随意命名,例如下面的 recur -

const U = f => f (f)

const Y = U (h => f => f (x => U (h) (f) (x)))

const fibonacci = Y (recur => n =>
  n < 2 ? n : recur (n - 1) +  (n - 2))

console.log (fibonacci (10)) // 55


具有多个参数的U和Y组合子

在上面的示例中,我们看到了如何循环并传递参数以跟踪我们计算的“状态”。但是如果我们需要跟踪其他状态怎么办?

我们可以使用类似于数组或其他复合数据类型的东西...

const U = f => f (f)

const Y = U (h => f => f (x => U (h) (f) (x)))

const fibonacci = Y (f => ([a, b, x]) =>
  x === 0 ? a : f ([b, a + b, x - 1]))

// starting with 0 and 1, generate the 7th number in the sequence
console.log (fibonacci ([0, 1, 7])) 
// 0 1 1 2 3 5 8 13

但这样做是不好的,因为它暴露了内部状态(计数器ab)。如果我们只能调用fibonacci (7)来获得我们想要的答案就好了。

利用我们对柯里化函数(一次只接受一个参数的函数序列)的了解,我们可以轻松实现我们的目标,而无需修改Y的定义,也不需要依赖于复合数据或高级语言特性。

仔细看一下下面的fibonacci的定义。我们立即应用了绑定到ab上的01。现在,fibonacci只是在等待最后一个参数被提供,并将其绑定到x上。当我们进行递归时,我们必须调用f(a)(b)(x)(而不是f(a,b,x)),因为我们的函数处于柯里化形式。

const U = f => f (f)

const Y = U (h => f => f (x => U (h) (f) (x)))

const fibonacci = Y (f => a => b => x =>
  x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1)

console.log (fibonacci (7)) 
// 0 1 1 2 3 5 8 13

这种模式可用于定义各种函数。下面我们将看到使用Y组合器(rangereduce)定义的两个函数和reduce的一个导数map

const U = f => f (f)

const Y = U (h => f => f (x => U (h) (f) (x)))

const range = Y (f => acc => min => max =>
  min > max ? acc : f ([...acc, min]) (min + 1) (max)) ([])

const reduce = Y (f => g => y => ([x,...xs]) =>
  x === undefined ? y : f (g) (g (y) (x)) (xs))
  
const map = f =>
  reduce (ys => x => [...ys, f (x)]) ([])
  
const add = x => y => x + y

const sq = x => x * x

console.log (range (-2) (2))
// [ -2, -1, 0, 1, 2 ]

console.log (reduce (add) (0) ([1,2,3,4]))
// 10

console.log (map (sq) ([1,2,3,4]))
// [ 1, 4, 9, 16 ]


一切都是匿名的,哇哦

因为我们在这里使用的是纯函数,所以我们可以用其定义替换任何命名函数。看看当我们将斐波那契数列中的命名函数替换为它们的表达式时会发生什么。


/* const U = f => f (f)
 *
 * const Y = U (h => f => f (x => U (h) (f) (x)))
 *
 * const fibonacci = Y (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1)
 *
 */

/*
 * given fibonacci (7)
 *
 * replace fibonacci with its definition
 * Y (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
 *
 * replace Y with its definition
 * U (h => f => f (x => U (h) (f) (x))) (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
//
 * replace U with its definition
 * (f => f (f)) U (h => f => f (x => U (h) (f) (x))) (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
 */

let result =
  (f => f (f)) (h => f => f (x => h (h) (f) (x))) (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
  
console.log (result) // 13

就是这样 - 使用匿名函数递归计算fibonacci(7)


10
哦,我的天啊,在 JavaScript 里面有很多 Haskell。 - cdecompilador

16
可能最简单的方法是使用“匿名对象”: ```可能最简单的方法是使用“匿名对象”:```
({
  do: function() {
    console.log("don't run this ...");
    this.do();
  }
}).do();

您的全局空间完全没有受到污染。这很简单明了。而且您可以轻松地利用对象的非全局状态。

您还可以使用ES6对象方法使语法更加简洁。

({
  do() {
    console.log("don't run this ...");
    this.do();
  }
}).do();

注意:这不适用于 Array.mapArray.forEach。替代方案:我们不想污染全局范围,使用模块可以轻易实现此目的。如果将代码分成模块,函数就不会污染全局范围。它将保持未导出状态,并且只会“污染”其模块的空间。是的,我们会牺牲可读性,但至少它能够工作。 - Ahmed Shaqanbi

13
我不会将此作为内联函数来处理。这超出了好品味的范围,并且实际上并没有给你带来任何好处。
如果您真的必须要这样做,可以使用类似Fabrizio答案中的arguments.callee。但是这通常被认为是不可取的,在ECMAScript第五版的“严格模式”中被禁止。虽然ECMA 3和非严格模式并没有消失,但在严格模式下工作可以提供更多可能的语言优化。
也可以使用命名的内联函数:
(function foo(data){
    data++;
    var nothing = function() {
        foo(data);
    }
    nothing();
})();

然而,被命名的内联函数表达式也最好避免,因为IE的JScript会对它们做一些不好的事情。在上面的例子中,foo 在IE中错误地污染了父级作用域,而父级的 foo 是一个单独的实例,与在 foo 中看到的 foo 不同。

把这个代码放在匿名内联函数中有什么目的?如果你只想避免污染父级作用域,当然可以将你的第一个示例隐藏在另一个自调用的匿名函数(命名空间)中。你真的需要在每次递归时创建一个 nothing 的新副本吗?你可能会更好地使用一个包含两个简单相互递归函数的命名空间。


我同意,一个命名函数比arguments.callee更适合ECMAScript严格模式,也是出于优化的考虑,因为在每次递归时需要获取对调用者的引用(这可能会降低执行速度)。 - fcalderan
+1 对于诗意的赞赏,"挑战良好品味的边界" - (当然还有很好的信息)。 - Peter Ajtai
如果真的担心污染,那么考虑使用一个简单的前缀/后缀。由于它不在全局范围内(即使函数是顶级的,他应该已经有一个匿名函数包装他的整个代码),所以recur_foo这样的名称很难与父作用域中的函数发生冲突(或被误用)。 - gblazex
非常有趣 - http://jsfiddle.net/hck2A/ - IE在这种情况下确实会污染父级,就像你说的那样。从未意识到过。 - Peter Ajtai
1
@Peter:请查看http://kangax.github.com/nfe/(特别是“JScript bugs”)以获取更多关于此主题的信息。在IE9中,这个问题终于得到了解决(但仅限于IE9标准模式)。 - bobince

9
(function(data){
    var recursive = arguments.callee;
    data = data+1;
    var nothing = function() {
        recursive(data)
    }
    nothing();
})();

35
希望所有投票支持这个(技术上正确的)答案的人都能意识到arguments.callee存在的问题:在严格模式和ES5中,它是被禁止使用的。 - Pointy
在 ES5 中,arguments.callee 被弃用了,已被投票下降。 - Jaime Rodriguez
它在NodeJS中运行。只要它在固定的环境中以可预测的方式工作,我就不关心ES5。 - Angad
1
这是一个定时炸弹。就像上面的评论所建议的那样,没有所谓的“固定”环境。你几乎总是会因为成千上万个原因之一而进行升级。 - sampathsris

8
你可以这样做:

您可以进行以下操作:

(foo = function() { foo(); })()

或者在你的情况下:
(recur = function(data){
    data = data+1;
    var nothing = function() {
        if (data > 100) return; // put recursion limit
        recur(data);
    }
    nothing();
})(/* put data init value here */ 0);

你最好使用 var 语句先声明 recur。不知道这是否违反了问题的规则,但是如果现在没有 var 语句,那么在 ECMAScript 5 严格模式下会出现错误。 - Tim Down
我的最初评论包含 var 关键字,但一旦我测试了这段代码,它就会抛出错误,因为你不能在自调用块内声明变量,而我的方法依赖于未定义变量的自动声明,因此 @Pointy 的解决方案更正确。不过,我仍然投了 Fabrizio Calderan 的答案 ;) - ArtBIT
是的,执行(var recur = function() {...})();不起作用,因为它现在是一个语句而不是赋值表达式(返回分配的值)。我建议改为var recur; (recur = function() {...})(); - Tim Down

3

在某些情况下,您必须依靠匿名函数。 给出一个递归的map函数:

const map = f => acc => ([head, ...tail]) => head === undefined 
 ? acc
 : map (f) ([...acc, f(head)]) (tail);

const sqr = x => x * x;
const xs = [1,2,3,4,5];

console.log(map(sqr) ([0]) (xs)); // [0] modifies the structure of the array

请注意,map不能修改数组的结构。因此累加器acc不需要暴露出来。我们可以将map包装在另一个函数中,例如:
const map = f => xs => {
  let next = acc => ([head, ...tail]) => head === undefined
   ? acc
   : map ([...acc, f(head)]) (tail);

  return next([])(xs);
}

但是这个解决方案相当啰嗦。让我们使用被低估的U组合子:

const U = f => f(f);

const map = f => U(h => acc => ([head, ...tail]) => head === undefined 
 ? acc
 : h(h)([...acc, f(head)])(tail))([]);

const sqr = x => x * x;
const xs = [1,2,3,4,5];

console.log(map(sqr) (xs));

简洁易懂,不是吗?U非常简单,但有一个缺点就是递归调用有点难懂:sum(...)变成了h(h)(...) - 就是这样。


3
当您像这样声明匿名函数时:

(function () {
    // Pass
}());

这被认为是一个函数表达式,它有一个可选的名称(您可以使用该名称从内部调用它)。但由于它是一个函数表达式(而不是语句),因此它是匿名的(但有一个名称可供调用)。因此,这个函数可以调用自身:

(function foo () {
    foo();
}());
foo //-> undefined

“it stays anonymous” - 不是这样的。匿名函数没有名称。我明白 foo 没有在当前上下文中声明,但这或多或少是无关紧要的。带有名称的函数仍然是命名函数 - 不是 匿名的。 - Mulan

3
为什么不将函数传递给函数本身呢?
    var functionCaller = function(thisCaller, data) {
        data = data + 1;
        var nothing = function() {
            thisCaller(thisCaller, data);
        };
        nothing();
    };
    functionCaller(functionCaller, data);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接