mirror of
https://github.com/ruanyf/es6tutorial.git
synced 2024-04-21 12:32:22 +00:00
edit function/tco
This commit is contained in:
+103
-12
@@ -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)。
|
||||
|
||||
+1
-1
@@ -424,7 +424,7 @@ ESLint用于静态检查代码的语法和风格,安装命令如下。
|
||||
$ npm install --save-dev eslint babel-eslint
|
||||
```
|
||||
|
||||
然后,在项目根目录下,新建一个配置文件`.eslint`,在其中加入`parser`字段。
|
||||
然后,在项目根目录下,新建一个配置文件`.eslintrc`,在其中加入`parser`字段。
|
||||
|
||||
```javascript
|
||||
{
|
||||
|
||||
@@ -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): 如何自己实现尾递归优化
|
||||
|
||||
## 对象
|
||||
|
||||
|
||||
Reference in New Issue
Block a user