javascript:递归匿名函数?

Posted

技术标签:

【中文标题】javascript:递归匿名函数?【英文标题】:javascript: recursive anonymous function? 【发布时间】:2011-04-22 11:28:06 【问题描述】:

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

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

我想要一种方法来调用调用此函数的函数...我在某处看到过脚本(我不记得在哪里),它可以告诉您调用的函数的名称,但我不能立即回忆起这些信息。

【问题讨论】:

您有需要这个的原因还是只是好奇?在我看来,简单地给它一个名字会更清楚...... @thenduks:出于同样的原因,人们会使用匿名函数。只是有时递归是必要的。 很遗憾arguments.callee 的存在,而且这个functnio 并没有做任何有用的事情。我正在查找 Y combinator :P 。该死的,那些东西永远不会有用... 是的,正如 Kobi 链接的那样,使用诸如 Y 之类的定点组合器来执行不带 arguments.callee 的匿名递归函数。 请参阅w3future.com/weblog/stories/2002/02/22/… 以获取 JS 中 Y 组合器的示例。 【参考方案1】:

我不建议在任何实际用例中执行此操作,但作为一个有趣的练习,您实际上可以使用第二个匿名函数来执行此操作!

(f => f(f))(f => 
    data = data+1;
    var nothing = function() 
        f();
    
    nothing(f);
);

它的工作方式是我们将匿名函数作为参数传递给它自己,所以我们可以从它自己调用它。

【讨论】:

【参考方案2】:

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

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

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

你还可以使用 ES6 对象方法来使语法更简洁。

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

【讨论】:

【参考方案3】:

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 组合子

相关:the U and Y combinators explained using a mirror analogy

在上一节中,我们看到了如何使用 U 组合子将自引用递归转换为不依赖于命名函数的递归函数。必须记住始终将函数作为第一个参数传递给自身,这有点令人烦恼。嗯,Y-combinator 建立在 U-combinator 的基础上,去掉了那个乏味的部分。这是一件好事,因为消除/减少复杂性是我们制作函数的主要原因

首先,让我们推导出我们自己的 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 的比较。注意,为了重复,我们可以简单地调用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 ()))

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

多于 1 个参数的 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,它们分别绑定到ab。现在斐波那契只是等待提供最后一个参数,该参数将绑定到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)以及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)只使用匿名函数递归计算

【讨论】:

天哪,javascript里面有很多haskell【参考方案4】:

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

箭头表示匿名函数对我来说更具可读性:

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

【讨论】:

【参考方案5】:

在 ES2015 中,我们可以使用一些语法和滥用默认参数和 thunk。后者只是没有任何参数的函数:

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) =&gt; n === 0 ? x : f(y, x + y, n - 1) 为默认值的参数。当fapplyT 调用时,此调用必须在没有参数的情况下进行,以便使用默认值。默认值是一个函数,因此f 是一个命名函数,它可以递归调用自身。

【讨论】:

【参考方案6】:

这是@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'))

【讨论】:

【参考方案7】:

在某些情况下,您必须依赖匿名函数。 Given 是一个递归的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)(...) - 仅此而已。

【讨论】:

【参考方案8】:

这是 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 的原始软泥。

【讨论】:

【参考方案9】:

我需要(或者更确切地说,想要)一个单行匿名函数来向上构建一个字符串的对象,并像这样处理它:

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

它会产生一个类似'Root : foo : bar : baz : ...'的字符串

【讨论】:

【参考方案10】:

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

所以,阶乘可以这样完成:

var fac = function(x)  
    if (x == 1) return x;
    else return x * arguments.callee(x-1);

【讨论】:

【参考方案11】:

另一个不涉及命名函数或arguments.callee的答案

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

【讨论】:

nice:将匿名函数绑定到本地参数,然后通过本地参数调用函数,同时也将函数传递给自身进行递归。【参考方案12】:

我不确定是否仍然需要答案,但这也可以使用使用 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。

【讨论】:

【参考方案13】:

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

(function foo()  foo(); )();

是一个堆栈式递归函数。现在,也就是说,您通常是probably don't may not want to do this,因为 Javascript 的各种实现存在一些奇怪的问题。 (注意 - 这是一个相当老的评论;Kangax 的博客文章中描述的一些/许多/所有问题可能会在更现代的浏览器中得到修复。)

当您给出这样的名称时,该名称在函数之外是不可见的(嗯,它不应该是;这是奇怪的地方之一)。就像 Lisp 中的“letrec”。

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

edit——如果你想要一个可以调用自身的“匿名”函数的效果,你可以做这样的事情(假设你将函数作为回调或类似的东西传递那个):

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

这样做是用一个漂亮、安全、不会在 IE 中破坏的函数 declaration 语句定义一个函数,创建一个名称不会污染全局命名空间的本地函数。包装器(真正匿名)函数只返回该本地函数。

【讨论】:

我们能否避免用 ES5 严格的方式污染全局命名空间(我还没有深入阅读 ES5)? @pointy 你能看看这个任务吗? ***.com/questions/27473450/… 我猜不可能使用(() =&gt; call_recursively_self_here() )() 并递归调用自身,对吧?我必须给它一个名字。 @Qwerty 好吧,你可以做一些类似于我回答中最后一个例子的事情。将箭头函数绑定到包装函数中的局部变量,以便您的箭头函数可以使用变量名引用自身。然后包装器将返回变量(指的是箭头函数)。 @Pointy 也许一些黑客会找到应用程序 ;)【参考方案14】:

为什么不将函数传递给函数本身?

    var functionCaller = function(thisCaller, data) 
        data = data + 1;
        var nothing = function() 
            thisCaller(thisCaller, data);
        ;
        nothing();
    ;
    functionCaller(functionCaller, data);

【讨论】:

【参考方案15】:

人们在 cmets 中谈到了 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();
  
)());

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

【讨论】:

“关于这个解决方案最重要的一点是你不应该使用它。” 为什么? 不会很快。实际使用起来很丑(尽管在概念上很漂亮!)。您避免必须为您的函数指定标签或变量名称(我不明白为什么会担心),但您仍然给它一个名称作为传递给 Y 的外部函数的参数。所以您不需要经历所有这些麻烦会有所收获。 别忘了提到这个函数不是堆栈安全的。只循环几千次就会导致堆栈溢出。 嗨,我建议稍微“更干净”的修改,因为 .apply(null,arguments) 对我来说看起来很丑: var Y = function (gen) return (function(f) return f(f) ); (function(f) return gen(function(x) return f(f)(x); ); ));或者等效地 (( function(x)return y equals (x => y) )) 通过使用箭头符号(有效的 js 代码): var Y = gen => ( f => f(f) ) ( f = > gen ( x => f(f)(x) ) )【参考方案16】:

当你像这样声明一个匿名函数时:

(function () 
    // Pass
());

它被认为是一个函数表达式,它有一个可选的名称(你可以用它来从它自身内部调用它。但是因为它是一个函数表达式(而不是一个语句)它保持匿名(但有一个你可以调用的名称) ). 所以这个函数可以调用自己:

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

【讨论】:

“它保持匿名” – 不,它没有。匿名函数没有名称。我知道foo 没有在当前上下文中声明,但这或多或少无关紧要。一个有名字的函数仍然是一个命名函数——不是匿名的。【参考方案17】:

就像 bobince 写的,简单地命名你的函数。

但是,我猜你也想传入一个初始值并最终停止你的函数!

var initialValue = ...

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

working jsFiddle example (uses data += data for fun)

【讨论】:

+1,这是一个非常有用的答案,你应该得到更多的支持,但它不是匿名的。 您显然没有阅读 bobince 写的内容:However named inline function expressions are also best avoided.。但是 OP 也没有抓住重点...... :) @Galamb - 我读过它。在严格模式和 ES5 中禁止与污染父范围和创建额外实例不同。【参考方案18】:

你可以这样做:

(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 声明。不知道这是否违反了问题的规则,但正如您现在所拥有的那样,如果没有 var 语句,您将在 ECMAScript 5 严格模式下收到错误。 我最初的评论包括 var 关键字,但是一旦我测试了这段代码,它就会抛出错误,因为你不能真正在自调用块内声明一个变量,我的方法依赖于未定义变量的自动声明,因此@Pointy 的解决方案更正确。但我仍然投票给 Fabrizio Calderan 的答案 ;) 是的,(var recur = function() ...)(); 是行不通的,因为它现在是一个语句而不是一个赋值表达式(它返回所分配的值)。我建议改为var recur; (recur = function() ...)();【参考方案19】:

我不会将其作为内联函数来执行。它打破了良好品味的界限,并没有真正让你得到任何东西。

如果你真的必须,有 arguments.callee 在 Fabrizio 的回答中。然而,这通常被认为是不可取的,并且在 ECMAScript 第五版的“严格模式”中是不允许的。尽管 ECMA 3 和非严格模式不会消失,但在严格模式下工作有望实现更多可能的语言优化。

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

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

但是,最好避免命名内联函数表达式,因为 IE 的 JScript 对它们做了一些坏事。在上面的示例中,foo 错误地污染了 IE 中的父作用域,而父作用域 foofoofoo 中看到的独立实例。

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

【讨论】:

我同意,命名函数比 arguments.callee 更合适,不仅适用于 ecmascript 严格模式,还适用于优化问题,因为在每次递归时,他都需要获取对被调用者的引用 (这可能会降低执行速度) +1 表示诗意,"pushing against the boundaries of good taste" - (嗯,很好的信息)。 如果污染真的是一个问题,那么简单的前/后缀呢?考虑到它不在全局范围内(即使函数是*** lvl,他应该已经有一个匿名函数来包装他的整个代码),像 recur_foo 这样的名称真的不太可能与父范围内的函数发生冲突(或被滥用)。 非常有趣 - jsfiddle.net/hck2A - 在这种情况下,IE 确实污染了父级,就像你说的那样。从来没有意识到这一点。 @Peter:kangax.github.com/nfe(尤其是“JScript 错误”)对于这个主题的了解比你想知道的要多。它终于在 IE9 中修复(但仅在 IE9 标准模式下)。【参考方案20】:
(function(data)
    var recursive = arguments.callee;
    data = data+1;
    var nothing = function() 
        recursive(data)
    
    nothing();
)();

【讨论】:

我希望每个投票支持这个(技术上正确)答案的人都意识到arguments.callee 的问题:在严格模式和 ES5 中是不允许的。 投票否决,arguments.callee 在 ES5 中已弃用 它在 NodeJS 中工作。只要 ES5 在固定环境中以可预测的方式运行,我就不会关心它。 这是一个定时炸弹。正如上面的评论所暗示的那样,没有所谓的“固定”环境。由于成千上万的原因,您几乎总是会升级。

以上是关于javascript:递归匿名函数?的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript-函数(调用参数returnarguments匿名回调递归函数)函数案例

JavaScript学习:函数表达式

python基础——匿名函数及递归函数

JavaScript函数进阶回调函数递归函数闭包函数

python 匿名函数和递归函数

七 递归与二分法匿名函数内置函数