当前位置:首页 > 日记 > 正文

JavaScript中匿名函数的递归调用

JavaScript中匿名函数的递归调用

不管是什么编程语言,相信稍微写过几行代码的同学,对递归都不会陌生。 以一个简单的阶乘计算为例:

function factorial(n) {   if (n <= 1) {    return 1;  } else {    return n * factorial(n-1);  }}

我们可以看出,递归就是在函数内部调用对自身的调用。 那么问题来了,我们知道在Javascript中,有一类函数叫做匿名函数,没有名称,怎么调用呢?当然你可以说,可以把匿名函数赋值给一个常量:

const factorial = function(n){    if (n <= 1) {    return 1;  } else {    return n * factorial(n-1);  }}

这当然是可以的。但是对于一些像,函数编写时并不知道自己将要赋值给一个明确的变量的情况时,就会遇到麻烦了。如:

(function(f){  f(10);})(function(n){   if (n <= 1) {    return 1;  } else {    return n * factorial(n-1);//太依赖于上下文变量名  }})//Uncaught ReferenceError: factorial is not defined(…)

那么存不存在一种完全不需要这种给予准确函数名(函数引用变量名)的方式呢?

arguments.callee

我们知道在任何一个function内部,都可以访问到一个叫做arguments的变量。

(function(){console.dir(arguments)})(1,2)

屏幕快照 2016-09-18 下午10.53.58

打印出这个arguments变量的细节,可以看出他是Arguments的一个实例,而且从数据结构上来讲,他是一个类数组。他除了类数组的元素成员和length属性外,还有一个callee方法。 那么这个callee方法是做什么的呢?我们来看下MDN

callee 是 arguments 对象的属性。在该函数的函数体内,它可以指向当前正在执行的函数。当函数是匿名函数时,这是很有用的, 比如没有名字的函数表达式 (也被叫做”匿名函数”)。

哈哈,很明显这就是我们想要的。接下来就是:

(function(f){  console.log(f(10));})(function(n){   if (n <= 1) {    return 1;  } else {    return n * arguments.callee(n-1);  }})//output: 3628800

但是还有一个问题,MDN的文档里明确指出

警告:在 ECMAScript 第五版 (ES5) 的 严格模式 中禁止使用 arguments.callee()。

哎呀,原来在ES5的use strict;中不给用啊,那么在ES6中,我们换个ES6的arrow function写写看:

((f) => console.log(f(10)))(  (n) => n <= 1? 1: arguments.callee(n-1))//Uncaught ReferenceError: arguments is not defined(…)

有一定ES6基础的同学,估计老早就想说了,箭头函数就是个简写形式的函数表达式,并且它拥有词法作用域的this值(即不会新产生自己作用域下的this, arguments, super 和 new.target等对象),且都是匿名的。

那怎么办呢?嘿嘿,我们需要借助一点FP的思想了。

Y组合子

关于Y Combinator的文章可谓数不胜数,这个由师从希尔伯特的著名逻辑学家Haskell B.Curry(Haskell语言就是以他命名的,而函数式编程语言里面的Curry手法也是以他命名)“发明”出来的组合算子(Haskell是研究组合逻辑(combinatory logic)的)仿佛有种神奇的魔力,它能够算出给定lambda表达式(函数)的不动点。从而使得递归成为可能。

这里需要告知一个概念不动点组合子

不动点组合子(英语:Fixed-point combinator,或不动点算子)是计算其他函数的一个不动点的高阶函数。

函数f的不动点是一个值x使得f(x) = x。例如,0和1是函数 f(x) = x^2 的不动点,因为 0^2 = 0而 1^2 = 1。鉴于一阶函数(在简单值比如整数上的函数)的不动点是个一阶值,高阶函数f的不动点是另一个函数g使得f(g) = g。那么,不动点算子是任何函数fix使得对于任何函数f都有

f(fix(f)) = fix(f). 不动点组合子允许定义匿名的递归函数。它们可以用非递归的lambda抽象来定义.

在无类型lambda演算中众所周知的(可能是最简单的)不动点组合子叫做Y组合子。

接下来,我们通过一定的演算推到下这个Y组合子。

// 首先我们定义这样一个可以用作求阶乘的递归函数const fact = (n) => n<=1?1:n*fact(n-1) console.log(fact(5)) //120// 既然不让这个函数有名字,我们就先给这个递归方法一个叫做self的代号// 首先是一个接受这个递归函数作为参数的一个高阶函数const fact_gen = (self) => (n) => n<=1?1:n*self(n-1) console.log(fact_gen(fact)(5)) //120// 我们是将递归方法和参数n,都传入递归方法,得到这样一个函数const fact1 = (self, n) => n<=1?1:n*self(self, n-1) console.log(fact1(fact1, 5)) //120// 我们将fact1 柯理化,得到fact2const fact2 = (self) => (n) => n<=1?1:n*self(self)(n-1) console.log(fact2(fact2)(5)) //120// 惊喜的事发生了,如果我们将self(self)看做一个整体// 作为参数传入一个新的函数: (g)=> n<= 1? 1: n*g(n-1)const fact3 = (self) => (n) => ((g)=>n <= 1?1:n*g(n-1))(self(self)) console.log(fact3(fact3)(5)) //120// fact3 还有一个问题是这个新抽离出来的函数,是上下文有关的// 他依赖于上文的n, 所以我们将n作为新的参数// 重新构造出这么一个函数: (g) => (m) => m<=1?1:m*g(m-1)const fact4 = (self) => (n) => ((g) => (m) => m<=1?1:m*g(m-1))(self(self))(n) console.log(fact4(fact4)(5))// 很明显fact4中的(g) => (m) => m<=1?1:m*g(m-1) 就是 fact_gen// 这就很有意思啦,这个fact_gen上下文无关了, 可以作为参数传入了const weirdFunc = (func_gen) => (self) => (n) => func_gen(self(self))(n) console.log(weirdFunc(fact_gen)(weirdFunc(fact_gen))(5)) //120// 此时我们就得到了一种Y组合子的形式了const Y_ = (gen) => (f) => (n)=> gen(f(f))(n)// 构造一个阶乘递归也很easy了const factorial = Y_(fact_gen) console.log(factorial(factorial)(5)) //120// 但上面这个factorial并不是我们想要的// 只是一种fact2,fact3,fact4的形式// 我们肯定希望这个函数的调用是factorial(5)// 没问题,我们只需要把定义一个 f' = f(f) = (f)=>f(f)// eg. const factorial = fact2(fact2)const Y = gen => n => (f=>f(f))(gen)(n) console.log(Y(fact2)(5)) //120 console.log(Y(fact3)(5)) //120 console.log(Y(fact4)(5)) //120

推导到这里,是不是已经感觉到脊背嗖凉了一下,反正笔者我第一次接触在康托尔、哥德尔、图灵——永恒的金色对角线这篇文章里接触到的时候,整个人瞬间被这种以数学语言去表示程序的方式所折服。

来,我们回忆下,我们最终是不是得到了一个不定点算子,这个算子可以找出一个高阶函数的不动点f(Y(f)) = Y(f)。 将一个函数传入一个算子(函数),得到一个跟自己功能一样,但又并不是自己的函数,这个说法有些拗口,但又味道十足。

好了,我们回到最初的问题,怎么完成匿名函数的递归呢?有了Y组合子就很简单了:

(f => f(f))(fact => n => n <= 1 ? 1 : n * fact(fact)(n - 1)) (5)// 120

曾经看到过一些说法是”最让人沮丧是,当你推导出它(Y组合子)后,完全没法儿通过只看它一眼就说出它到底是想干嘛”,而我恰恰认为这就是函数式编程的魅力,也是数学的魅力所在,精简优雅的公式,背后隐藏着复杂有趣的推导过程。

总结

务实点儿讲,匿名函数的递归调用,在日常的js开发中,用到的真的很少。把这个问题拿出来讲,主要是想引出对arguments的一些讲解和对Y组合子这个概念的一个普及。

但既然讲都讲了,我们真的用到的话,该怎么选择呢?来,我们喜闻乐见的benchmark下: 分别测试:

// fact fact(10) // Y(f => f(f))(fact => n => n <= 1 ? 1 : n * fact(fact)(n - 1))(10)// Y'const fix = (f) => f(f) const ygen = fix(fact2) ygen(10) // callee(function(n) {n<=1?1:n*arguments.callee(n-1)})(10)

环境:Macbook pro(2.5 GHz Intel Core i7), node-5.0.0(V8:4.6.85.28) 结果:

fact x 18,604,101 ops/sec ±2.22% (88 runs sampled)Y x 2,799,791 ops/sec ±1.03% (87 runs sampled)Y' x 3,678,654 ops/sec ±1.57% (77 runs sampled)callee x 2,632,864 ops/sec ±0.99% (81 runs sampled)

可见Y和callee的性能相差不多,因为需要临时构建函数,所以跟直接的fact递归调用有差不多一个数量级的差异,将不定点函数算出后保存下来,大概会有一倍左右的性能提升。

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持!

相关文章

Excel制作迟到早退旷工的考勤表

Excel制作迟到早退旷工的考勤表

考勤表,电脑软件,Excel,Excel里面,如何根据打开的时间,算出员工是否迟到、早退或是旷工呢?这样的考勤表如何使用Excel来做。下面我们先看下表。上表中,A列记录的是来上班的时间,即进入公司就打卡,该时间是上班的时间。B列是员工离开公司的时间,…

PS合成童话故事中毛骨悚然的悬浮场

PS合成童话故事中毛骨悚然的悬浮场

毛骨悚然,童话故事,场景,电脑软件,PS,除了令人毛骨悚然的悬浮房屋,你会掌握如何创建一些非常酷的3D风格的字体效果,如何有效地结合你的场景,以及如何使用透视线的角度。最终效果1、新建1300 * 1833 px,分辨率为150 px文件。 2、把素材拖入文档,…

win10分辨率设置

win10分辨率设置

设置,分辨率,电脑软件,win10分辨率怎么设置呢?首先我们需要更新一下显卡驱动,让win10系统识别最优的分辨率状态,接下来的步骤让小编详细的介绍win10分辨率设置的方法。win10分辨率设置方法步骤如下:1.点击右下角的&ldquo;通知中心图标&rdquo;>…

Angularjs 实现移动端在线测评效果

Angularjs 实现移动端在线测评效果

移动端,推荐,在线,效果,电脑软件,注:此文所用的angular版本为 1.6一、运行效果图二、需求1. 点击选项时,背景变为黄色(即选中状态),并且自动切换到下一题2. 切换到下一题时,顶部进度随之改变3. 选中时要把对应的分值记录下来(因为要根据分值算出最…

JavaScript中Hoisting详解  | 变量

JavaScript中Hoisting详解 | 变量

函数声明,提升,变量提升,详解,电脑软件,本文主要给大家介绍了关于JavaScript中Hoisting(变量提升与函数声明提升)的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧。如何将 函数声明 / 变量 “移动” 到作用域的顶部…

ppt2010怎样制作艺术字水印

ppt2010怎样制作艺术字水印

方法,水印,艺术字,电脑软件,  ppt2010如何用艺术字做水印呢?这时我们需要用到母板的功能,具体怎么做,对于不常用对于不常用ppt的朋友或许有点难度,下面小编来告诉你ppt2010用艺术字做水印的方法吧。ppt2010用艺术字做水印的方法在&ldquo;视…

php大小写转换函数 | strtolower、

php大小写转换函数 | strtolower、

大小写,转换函数,电脑软件,php,strtoupper,1,将字符串转换成小写strtolower函数: 该函数将传入的字符串参数所有的字符都转换成小写,并以小定形式放回这个字符串。例子:<?php $str = "I want To FLY"; $str = strtolower($str); echo $st…

jQuery 实现鼠标画框并对框内数据

jQuery 实现鼠标画框并对框内数据

数据,实例代码,鼠标,并对,电脑软件,jquery库:jquery -1.10.2.min.js,jQuery UI - v1.12.1。jQuery 代码不多说了,之间上代码。不懂的地方看注释。<script type="text/javascript"> //鼠标按下时的X Y坐标 var mouseDownX; var mouseDownY; /…

WebService传XML 简单实例

WebService传XML 简单实例

简单实例,电脑软件,WebService,XML,WebService传XML 简单实例传送 [WebMethod]public XmlDataDocument GetSiteAData(string AssignName) { XmlDataDocument xd = new XmlDataDocument(); DataSet ds = BusinessWork.BusinessWor…

Windows7 IIS+ASP http500内部服务

Windows7 IIS+ASP http500内部服务

显示,错误,服务器,本来面目,电脑软件,在WINDOWS 7上安装了IIS7.5,调试ASP程序时出现http500内部服务器错误:首先,打开IE选项设置&mdash;高级&mdash;把&ldquo;显示友好http错误信息&rdquo;,可以看到如下错误提示:解决办法是打开将错误送到浏览器…

详谈jQuery unbind 删除绑定事件 /

详谈jQuery unbind 删除绑定事件 /

标签,删除,绑定事件,方法,移除,jQuery unbind 删除绑定事件unbind([type],[data]) 是 bind()的反向操作,从每一个匹配的元素中删除绑定的事件。如果没有参数,则删除所有绑定的事件。你可以将你用bind()注册的自定义事件取消绑定。如果提供了…

给Ajax返回的HTML标签动态添加样式

给Ajax返回的HTML标签动态添加样式

标签,方法,动态添加,样式,电脑软件,今天在做项目时,在页面中用ajax返回了另一个页面,突然发现返回页面中的表格把页面给顶了出去,经过一番研究,终于解决了。先准备好要返回内容的容器<div class="container"> </div>预定义一个样式,以便返回的…