JavaScript中匿名函数的递归调用


Posted in Javascript onJanuary 22, 2017

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

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 柯理化,得到fact2
const 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递归调用有差不多一个数量级的差异,将不定点函数算出后保存下来,大概会有一倍左右的性能提升。

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

Javascript 相关文章推荐
利用javascript实现一些常用软件的下载导航
Aug 03 Javascript
javascript 无提示关闭窗口脚本
Aug 17 Javascript
juqery 学习之三 选择器 子元素与表单
Nov 25 Javascript
jquery五角星评分插件示例分享
Feb 21 Javascript
jQuery常用数据处理方法小结
Feb 20 Javascript
js完美实现@提到好友特效(兼容各大浏览器)
Mar 16 Javascript
jQuery实现企业网站横幅焦点图切换功能实例
Apr 30 Javascript
JS打字效果的动态菜单代码分享
Aug 21 Javascript
Bootstrap源码解读下拉菜单(4)
Dec 23 Javascript
vuejs使用$emit和$on进行组件之间的传值的示例
Oct 04 Javascript
javascript Canvas动态粒子连线
Jan 01 Javascript
js实现简单选项卡制作
Aug 05 Javascript
Javascript中字符串和数字的操作方法整理
Jan 22 #Javascript
loading动画特效小结
Jan 22 #Javascript
全面总结Javascript对数组对象的各种操作
Jan 22 #Javascript
通过jsonp获取json数据实现AJAX跨域请求
Jan 22 #Javascript
JS实现点击表头表格自动排序(含数字、字符串、日期)
Jan 22 #Javascript
node.js基于mongodb的搜索分页示例
Jan 22 #Javascript
利用JS实现文字的聚合动画效果
Jan 22 #Javascript
You might like
用户的详细注册和判断
2006/10/09 PHP
PHP捕获Fatal error错误的方法
2014/06/11 PHP
PHP5.6新增加的可变函数参数用法分析
2017/08/25 PHP
PHP实现数组的笛卡尔积运算示例
2017/12/15 PHP
jquery $.ajax各个事件执行顺序
2010/10/15 Javascript
调试Javascript代码(浏览器F12及VS中debugger关键字)
2013/01/25 Javascript
js 日期比较相关天数代码
2014/04/02 Javascript
js中的getAttribute方法使用示例
2014/08/01 Javascript
javascript几个易错点记录
2014/11/26 Javascript
20个实用的JavaScript技巧分享
2014/11/28 Javascript
JavaScript判断FileUpload控件上传文件类型
2015/09/28 Javascript
javascript的列表切换【实现代码】
2016/05/03 Javascript
通用无限极下拉菜单的实现代码
2016/05/31 Javascript
jQuery使用正则表达式限制文本框只能输入数字
2016/06/18 Javascript
javascript 操作cookies详解及实例
2017/02/22 Javascript
vue.js实现价格格式化的方法
2017/05/23 Javascript
解决Layui 表格自适应高度的问题
2019/11/15 Javascript
Vue路由 重定向和别名的区别说明
2020/09/09 Javascript
python实现文件分组复制到不同目录的例子
2014/06/04 Python
python实现批量改文件名称的方法
2015/05/25 Python
使用python将大量数据导出到Excel中的小技巧分享
2018/06/14 Python
python实现自动网页截图并裁剪图片
2018/07/30 Python
Python中__slots__属性介绍与基本使用方法
2018/09/05 Python
python实现中文文本分句的例子
2019/07/15 Python
python3爬取torrent种子链接实例
2020/01/16 Python
关于Kotlin中SAM转换的那些事
2020/09/15 Python
Weblogic和WebSphere不同特点
2012/05/09 面试题
行政助理的岗位职责
2014/02/18 职场文书
工程师岗位职责规定
2014/02/26 职场文书
社区党务公开实施方案
2014/03/18 职场文书
《分一分》教学反思
2014/04/13 职场文书
合作协议书范文
2014/08/20 职场文书
乡镇党的群众路线教育实践活动制度建设计划
2014/11/03 职场文书
财务人员个人工作总结
2015/02/27 职场文书
个人求职信格式范文
2015/03/20 职场文书
Lakehouse数据湖并发控制陷阱分析
2022/03/31 Oracle