javascript:递归匿名函数?


120

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

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();
})();

我想要一种调用该函数的函数的方法...我曾经在某个地方(我不记得在哪里)看到过脚本,这些脚本可以告诉您所调用的函数的名称,但是我不记得其中任何一个该信息现在。


您有这个理由吗?还是只是好奇?在我看来,简单地给它一个名字会更清楚...
rfunduk

1
@thenduks:出于同样的原因,为什么要使用匿名函数。只是有时需要递归。

5
这是一种可耻的arguments.callee存在,并且此功能没有任何用处。我正在查找Y组合器 :P。该死的,这些东西永远不会有用...
Kobi 2010年

1
是的,正如Kobi所链接的那样,使用诸如Y的定点组合器来执行不带参数的匿名递归函数。
steamer25年

1
有关JS中的Y组合器的示例,请参见w3future.com/weblog/stories/2002/02/22/…
steamer25年

Answers:


145

可以给功能的名字,甚至当你正在创建的函数作为值,而不是一个“函数声明”的声明。换一种说法:

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

是一个栈吹动的递归函数。话虽如此,您可能一般都不希望这样做,因为Javascript的各种实现存在一些奇怪的问题。(请注意,这是一个相当古老的评论; Kangax博客文章中描述的一些/很多/所有问题都可能在更现代的浏览器中得以解决。)

当您提供这样的名称时,该名称在函数外部不可见(嗯,这不应该是;这是很奇怪的现象之一)。就像Lisp中的“ letrec”一样。

至于arguments.callee,这在“严格”模式下是不允许的,通常被认为是一件坏事,因为它会使某些优化工作变得困难。它也比人们预期的要慢得多。

编辑 -如果您想获得一个可以调用自身的“匿名”函数的效果,则可以执行以下操作(假设您将函数作为回调传递或诸如此类):

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

所要做的就是用一个不错的,安全的,不中断的IE函数声明语句定义一个函数,创建一个本地函数,其名称不会污染全局名称空间。包装器(真正匿名)仅返回该局部函数。


我们是否可以避免使用ES5严格性以另一种方式污染全局名称空间(我还没有深入了解ES5)?
隐身于2010年

@pointy您能否看一下这个问题。stackoverflow.com/questions/27473450/...
Gladson罗宾逊

我想不可能(() => { call_recursively_self_here() })()递归使用和调用自己,对吧?我必须给它起个名字。
Qwerty

1
@Qwerty好,您可以在我的答案中做最后一个例子。将arrow函数绑定到包装函数中的局部变量,以便您的arrow函数可以使用变量名引用自身。然后,包装器将返回变量(引用箭头函数)。
尖尖的2016年

1
@Pointy也许有些黑客会找到应用程序;)
KamilKiełczewski19年

31

人们在评论中谈到了Y组合器,但没人写它作为答案。

可以在javascript中定义Y组合器,如下所示:(感谢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();
  }
})());

关于此解决方案要注意的最重要的事情是,您不应该使用它。


16
“关于此解决方案,最重要的注意事项是您不应该使用它。” 为什么?
nyuszika7h 2011年

7
不会很快。实际使用起来很丑陋(尽管概念上很漂亮!)。您避免为函数指定标签或变量名(我不明白为什么会这样),但是您仍然给它起一个名字作为传递给Y的外部函数的参数。因此,您不必通过所有这些麻烦获得任何收益。
zem

不要忘了提到此功能不是堆栈安全的。循环几千次将导致堆栈溢出。
谢谢您

嗨,由于.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)))
myfirstAnswer

23

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组合器应用于自身时,会返回一个函数,等待第一个输入。如果我们为此命名,则可以使用lambdas(匿名函数)有效地构造递归函数。

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 loop (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 (x => Y (f) (x))

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

现在,我们将看到它的用法与U-combinator的比较。注意,要重现,U (f)我们可以简单地调用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 ()))

现在,我将使用演示该countDown程序Y–您会看到程序几乎相同,但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

现在,我们可以看到factorial,以及

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)得到我们想要的答案,那将是很好。

使用我们对咖喱函数(一元(1-参数)函数的序列)的了解,我们可以轻松实现目标,而无需修改我们Y对复合数据或高级语言功能的定义或依赖。

fibonacci仔细查看下面的定义。我们将立即应用01分别绑定到a和的对象b。现在,斐波那契只是在等待提供最后一个参数,该参数将被绑定到x。递归时,我们必须调用f (a) (b) (x)(not 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)和衍生物reducemap

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)使用匿名函数就可以递归计算


14

改为使用“匿名对象”可能是最简单的:

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

您的全球空间完全不受污染。这很简单。而且,您可以轻松利用对象的非全局状态。

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

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

13

我不会将其作为内联函数来执行。它正在突破好口味的界限,并没有真正给您带来任何好处。

如果确实需要,可以arguments.callee按照Fabrizio的答案进行。但是,通常认为这是不可取的,在ECMAScript第五版的“严格模式”中是不允许的。尽管ECMA 3和非严格模式并没有消失,但在严格模式下工作有望带来更多可能的语言优化。

也可以使用命名的内联函数:

(function foo(data){
    data++;
    var nothing = function() {
        foo(data);
    }
    nothing();
})();

但是,最好也避免使用命名的内联函数表达式,因为IE的JScript对它们有一些不好的作用。在上面的示例中,foo错误地污染了IE中的父级作用域,并且父级foofoo内部可见的单独实例foo

将其放在嵌入式匿名函数中的目的是什么?如果只想避免污染父范围,则当然可以将第一个示例隐藏在另一个自调用匿名函数(名称空间)中。您是否真的需要nothing在递归中每次都创建一个新副本?使用包含两个简单的相互递归函数的名称空间可能会更好。


我同意,命名函数比arguments.callee更合适.callee不仅适用于ecmascript严格模式,而且还涉及优化问题,因为在每次递归时,他都需要引用被调用者(这可能会降低执行速度)

对于诗意的人,+ 1 "pushing against the boundaries of good taste"(-和好的信息)。
彼得·阿杰泰

如果污染确实是一个令人担忧的问题,那么简单的前缀/后缀怎么办?考虑到它不在全局范围内(即使该函数是顶级lvl,他应该已经有一个匿名函数包装了他的整个代码),像这样的名称实际上不太可能recur_foo与父范围内的函数发生冲突(或生病) -用过的) 。
gblazex

非常有趣-jsfiddle.net/hck2A-在这种情况下,IE确实会污染父级,就像您说的那样。从来没有意识到这一点。
彼得·阿杰泰

1
@Peter:kangax.github.com/nfe(尤其是“ JScript错误”)的内容比您想了解的要多。终于在IE9中修复了(但仅在IE9标准模式下)。
bobince 2010年

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

34
我希望投票给这个(技术上正确)答案的人都意识到以下问题arguments.callee:在严格模式和ES5中不允许这样做。
尖尖的

投票否决,arguments.callee在ES5中已弃用
Jaime Rodriguez

它可以在NodeJS中工作。只要ES5在固定环境中以可预测的方式工作,我就不会在乎它。
Angad

1
这是一颗定时炸弹。如上面的注释所建议的,没有所谓的“固定”环境。由于成千上万的原因,您几乎总是会升级。
sampathsris '18

6

您可以执行以下操作:

(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);

您可以recur先声明一条var语句。Dunno是否违反了问题的规则,但是正如您现在所拥有的那样,如果没有var声明,您将在ECMAScript 5严格模式下收到错误消息。
Tim Down

我最初的注释中包含了var关键字,但是一旦测试了此代码,它就会引发错误,因为您无法在自调用块内真正声明变量,并且我的方法依赖于未声明变量的自动声明,因此@Pointy解决方案更正确。但是我仍然投票赞成Fabrizio Calderan的答案;)
ArtBIT 2010年

是的,这样做是(var recur = function() {...})();行不通的,因为它现在是语句而不是赋值表达式(返回分配的值)。我是建议var recur; (recur = function() {...})();
Tim Down

3

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

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

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

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

“它保持匿名” –不,不是。匿名函数没有名称。我知道这foo不会在当前上下文中声明,但这或多或少都没有关系。具有名称的函数仍是命名函数- 不是匿名的。
谢谢您


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)(...)-仅此而已。


2

我不确定是否仍然需要答案,但是也可以使用使用function.bind创建的委托来完成:

    var x = ((function () {
        return this.bind(this, arguments[0])();
    }).bind(function (n) {
        if (n != 1) {
            return n * this.bind(this, (n - 1))();
        }
        else {
            return 1;
        }
    }))(5);

    console.log(x);

这不涉及命名函数或arguments.callee。


1

就像bobince所写,只需命名您的函数即可。

但是,我猜您也想传递一个初始值并最终停止您的功能!

var initialValue = ...

(function recurse(data){
    data++;
    var nothing = function() {
        recurse(data);
    }
    if ( ... stop condition ... )
        { ... display result, etc. ... }
    else
        nothing();
}(initialValue));

jsFiddle工作示例(使用数据+ =数据很有趣)



1
+1,这是一个非常有用的答案,您应该为此获得更多的赞誉,但这不是匿名的。
隐身

您显然没有阅读bobince写的内容:However named inline function expressions are also best avoided.。但是OP也没有
抓住

@Galamb-我读了。在严格模式下和ES5中不允许这样做与污染父作用域并创建额外的实例不同。
Peter Ajtai 2010年

1

我需要(或更确切地说,是想要)一个单行匿名函数来逐步建立一个构建字符串的对象,并按如下方式处理它:

var cmTitle = 'Root' + (function cmCatRecurse(cmCat){return (cmCat == root) ? '' : cmCatRecurse(cmCat.parent) + ' : ' + cmCat.getDisplayName();})(cmCurrentCat);

会产生类似“ Root:foo:bar:baz:...”的字符串


1

使用ES2015,我们可以在语法上稍作改动,并滥用默认参数和改进。后者只是没有任何参数的函数:

const applyT = thunk => thunk();

const fib = n => applyT(
  (f = (x, y, n) => n === 0 ? x : f(y, x + y, n - 1)) => f(0, 1, n)
);

console.log(fib(10)); // 55

// Fibonacci sequence: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55...

请注意,这f是一个以匿名函数(x, y, n) => n === 0 ? x : f(y, x + y, n - 1)为默认值的参数。当f被调用applyT这个调用必须发生不带参数,以便使用默认值。默认值是一个函数,因此f是一个命名函数,它可以递归调用自身。


0

另一个不涉及命名函数或参数的答案。

var sum = (function(foo,n){
  return n + foo(foo,n-1);
})(function(foo,n){
     if(n>1){
         return n + foo(foo,n-1)
     }else{
         return n;
     }
},5); //function takes two argument one is function and another is 5

console.log(sum) //output : 15

不错:将匿名函数绑定到本地参数,然后通过本地参数调用该函数,但也将函数传递给自身以进行递归。
englebart '16

0

这是对jforjs答案的重做,具有不同的名称和稍作修改的条目。

// function takes two argument: first is recursive function and second is input
var sum = (function(capturedRecurser,n){
  return capturedRecurser(capturedRecurser, n);
})(function(thisFunction,n){
     if(n>1){
         return n + thisFunction(thisFunction,n-1)
     }else{
         return n;
     }
},5); 

console.log(sum) //output : 15

无需展开第一次递归。接收自身作为参考的函数可以追溯到OOP的原始渗漏。


0

这是带有箭头功能的@zem答案的版本。

您可以使用UY组合器。Y组合器是最简单的使用。

U 组合器,您必须继续传递该函数: const U = f => f(f) U(selfFn => arg => selfFn(selfFn)('to infinity and beyond'))

Y 组合器,您不必继续传递函数: const Y = gen => U(f => gen((...args) => f(f)(...args))) Y(selfFn => arg => selfFn('to infinity and beyond'))


0

另一个Y-combinator解决方案,使用Rosetta-code链接(我认为以前有人在stackOverflow的某处提到了该链接。

箭头用于匿名功能,我更容易理解:

var Y = f => (x => x(x))(y => f(x => y(y)(x)));

-1

这可能不适用于任何地方,但是您可以arguments.callee用来引用当前函数。

因此,可以这样进行阶乘:

var fac = function(x) { 
    if (x == 1) return x;
    else return x * arguments.callee(x-1);
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.