From 0be4b183121169b445323ead38a8bea0ffffb1b5 Mon Sep 17 00:00:00 2001 From: ruanyf Date: Wed, 3 Feb 2016 17:41:36 +0800 Subject: [PATCH] edit function/tco --- docs/function.md | 115 +++++++++++++++++++++++++++++++++++++++++----- docs/intro.md | 2 +- docs/reference.md | 1 + 3 files changed, 105 insertions(+), 13 deletions(-) diff --git a/docs/function.md b/docs/function.md index 4e14beb..efd3207 100644 --- a/docs/function.md +++ b/docs/function.md @@ -1170,17 +1170,6 @@ factorial(5, 1) // 120 由此可见,“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6也是如此,第一次明确规定,所有ECMAScript的实现,都必须部署“尾调用优化”。这就是说,在ES6中,只要使用尾递归,就不会发生栈溢出,相对节省内存。 -注意,只有开启严格模式,尾调用优化才会生效。由于一旦启用尾调用优化,`func.arguments`和`func.caller`这两个函数内部对象就失去意义了,因为外层的帧会被整个替换掉,这两个对象包含的信息就会被移除。严格模式下,这两个对象也是不可用的。 - -```javascript -function restricted() { - "use strict"; - restricted.caller; // 报错 - restricted.arguments; // 报错 -} -restricted(); -``` - ### 递归函数的改写 尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量 total ,那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观,第一眼很难看出来,为什么计算5的阶乘,需要传入两个参数5和1? @@ -1244,11 +1233,113 @@ ES6的尾调用优化只在严格模式下开启,正常模式是无效的。 这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。 -- `arguments`:返回调用时函数的参数。 +- `func.arguments`:返回调用时函数的参数。 - `func.caller`:返回调用当前函数的那个函数。 尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。 +```javascript +function restricted() { + "use strict"; + restricted.caller; // 报错 + restricted.arguments; // 报错 +} +restricted(); +``` + +### 尾递归优化的实现 + +尾递归优化只在严格模式下生效,那么正常模式下,或者那些不支持该功能的环境中,有没有办法也使用尾递归优化呢?回答是可以的,就是自己实现尾递归优化。 + +它的原理非常简单。尾递归之所以需要优化,原因是调用栈太多,造成溢出,那么只要减少调用栈,就不会溢出。怎么做可以减少调用栈呢?就是采用“循环”换掉“递归”。 + +下面是一个正常的递归函数。 + +```javascript +function sum(x, y) { + if (y > 0) { + return sum(x + 1, y - 1); + } else { + return x; + } +} + +sum(1, 100000) +// Uncaught RangeError: Maximum call stack size exceeded(…) +``` + +上面代码中,`sum`是一个递归函数,参数`x`是需要累加的值,参数`y`控制递归次数。一旦指定`sum`递归100000次,就会报错,提示超出调用栈的最大次数。 + +蹦床函数(trampoline)可以将递归执行转为循环执行。 + +```javascript +function trampoline(f) { + while (f && f instanceof Function) { + f = f(); + } + return f; +} +``` + +上面就是蹦床函数的一个实现,它接受一个函数`f`作为参数。只要`f`执行后返回一个函数,就继续执行。注意,这里是返回一个函数,然后执行该函数,而不是函数里面调用函数,这样就避免了递归执行,从而就消除了调用栈过大的问题。 + +然后,要做的就是将原来的递归函数,改写为每一步返回另一个函数。 + +```javascript +function sum(x, y) { + if (y > 0) { + return sum.bind(null, x + 1, y - 1); + } else { + return x; + } +} +``` + +上面代码中,`sum`函数的每次执行,都会返回自身的另一个版本。 + +现在,使用蹦床函数执行`sum`,就不会发生调用栈溢出。 + +```javascript +trampoline(sum(1, 100000)) +// 100001 +``` + +蹦床函数并不是真正的尾递归优化,下面的实现才是。 + +```javascript +function tco(f) { + var value; + var active = false; + var accumulated = []; + + return function accumulator() { + accumulated.push(arguments); + if (!active) { + active = true; + while (accumulated.length) { + value = f.apply(this, accumulated.shift()); + } + active = false; + return value; + } + } +} + +var sum = tco(function(x, y) { + if (y > 0) { + return sum(x + 1, y - 1) + } + else { + return x + } +}); + +sum(1, 100000) +// 100001 +``` + +上面代码中,`tco`函数是尾递归优化的实现,它的奥妙就在于状态变量`active`。默认情况下,这个变量是不激活的。一旦进入尾递归优化的过程,这个变量就激活了。然后,每一轮递归`sum`返回的都是`undefined`,所以就避免了递归执行;而`accumulated`数组存放每一轮`sum`执行的参数,总是有值的,这就保证了`accumulator`函数内部的`while`循环总是会执行。这样就很巧妙地将“递归”改成了“循环”,而后一轮的参数会取代前一轮的参数,保证了调用栈只有一层。 + ## 函数参数的尾逗号 ES7有一个[提案](https://github.com/jeffmo/es-trailing-function-commas),允许函数的最后一个参数有尾逗号(trailing comma)。 diff --git a/docs/intro.md b/docs/intro.md index 7e30a62..1d34349 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -424,7 +424,7 @@ ESLint用于静态检查代码的语法和风格,安装命令如下。 $ npm install --save-dev eslint babel-eslint ``` -然后,在项目根目录下,新建一个配置文件`.eslint`,在其中加入`parser`字段。 +然后,在项目根目录下,新建一个配置文件`.eslintrc`,在其中加入`parser`字段。 ```javascript { diff --git a/docs/reference.md b/docs/reference.md index 64a4e62..20801f6 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -74,6 +74,7 @@ - Ragan Wald, [Destructuring and Recursion in ES6](http://raganwald.com/2015/02/02/destructuring.html): rest参数和扩展运算符的详细介绍 - Axel Rauschmayer, [The names of functions in ES6](http://www.2ality.com/2015/09/function-names-es6.html): 函数的name属性的详细介绍 - Kyle Simpson, [Arrow This](http://blog.getify.com/arrow-this/): 箭头函数并没有自己的this +- Mark McDonnell, [Understanding recursion in functional JavaScript programming](http://www.integralist.co.uk/posts/js-recursion.html): 如何自己实现尾递归优化 ## 对象