mirror of
https://github.com/ruanyf/es6tutorial.git
synced 2024-04-21 12:32:22 +00:00
docs(generator): edit typo
This commit is contained in:
+20
-21
@@ -1228,7 +1228,7 @@ function f(x){
|
||||
}
|
||||
```
|
||||
|
||||
上面代码中,函数f的最后一步是调用函数g,这就叫尾调用。
|
||||
上面代码中,函数`f`的最后一步是调用函数`g`,这就叫尾调用。
|
||||
|
||||
以下三种情况,都不属于尾调用。
|
||||
|
||||
@@ -1250,7 +1250,7 @@ function f(x){
|
||||
}
|
||||
```
|
||||
|
||||
上面代码中,情况一是调用函数g之后,还有赋值操作,所以不属于尾调用,即使语义完全一样。情况二也属于调用后还有操作,即使写在一行内。情况三等同于下面的代码。
|
||||
上面代码中,情况一是调用函数`g`之后,还有赋值操作,所以不属于尾调用,即使语义完全一样。情况二也属于调用后还有操作,即使写在一行内。情况三等同于下面的代码。
|
||||
|
||||
```javascript
|
||||
function f(x){
|
||||
@@ -1270,13 +1270,13 @@ function f(x) {
|
||||
}
|
||||
```
|
||||
|
||||
上面代码中,函数m和n都属于尾调用,因为它们都是函数f的最后一步操作。
|
||||
上面代码中,函数`m`和`n`都属于尾调用,因为它们都是函数`f`的最后一步操作。
|
||||
|
||||
### 尾调用优化
|
||||
|
||||
尾调用之所以与其他调用不同,就在于它的特殊的调用位置。
|
||||
|
||||
我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。
|
||||
我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数`A`的内部调用函数`B`,那么在`A`的调用帧上方,还会形成一个`B`的调用帧。等到`B`运行结束,将结果返回到`A`,`B`的调用帧才会消失。如果函数`B`内部还调用函数`C`,那就还有一个`C`的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。
|
||||
|
||||
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。
|
||||
|
||||
@@ -1298,7 +1298,7 @@ f();
|
||||
g(3);
|
||||
```
|
||||
|
||||
上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除 f(x) 的调用帧,只保留 g(3) 的调用帧。
|
||||
上面代码中,如果函数`g`不是尾调用,函数`f`就需要保存内部变量`m`和`n`的值、`g`的调用位置等信息。但由于调用`g`之后,函数`f`就结束了,所以执行到最后一步,完全可以删除`f(x)`的调用帧,只保留`g(3)`的调用帧。
|
||||
|
||||
这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。
|
||||
|
||||
@@ -1331,7 +1331,7 @@ function factorial(n) {
|
||||
factorial(5) // 120
|
||||
```
|
||||
|
||||
上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。
|
||||
上面代码是一个阶乘函数,计算`n`的阶乘,最多需要保存`n`个调用记录,复杂度 O(n) 。
|
||||
|
||||
如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。
|
||||
|
||||
@@ -1344,9 +1344,9 @@ function factorial(n, total) {
|
||||
factorial(5, 1) // 120
|
||||
```
|
||||
|
||||
还有一个比较著名的例子,就是计算fibonacci 数列,也能充分说明尾递归优化的重要性
|
||||
还有一个比较著名的例子,就是计算 Fibonacci 数列,也能充分说明尾递归优化的重要性。
|
||||
|
||||
如果是非尾递归的fibonacci 递归方法
|
||||
非尾递归的 Fibonacci 数列实现如下。
|
||||
|
||||
```javascript
|
||||
function Fibonacci (n) {
|
||||
@@ -1355,13 +1355,12 @@ function Fibonacci (n) {
|
||||
return Fibonacci(n - 1) + Fibonacci(n - 2);
|
||||
}
|
||||
|
||||
Fibonacci(10); // 89
|
||||
// Fibonacci(100)
|
||||
// Fibonacci(500)
|
||||
// 堆栈溢出了
|
||||
Fibonacci(10) // 89
|
||||
Fibonacci(100) // 堆栈溢出
|
||||
Fibonacci(500) // 堆栈溢出
|
||||
```
|
||||
|
||||
如果我们使用尾递归优化过的fibonacci 递归算法
|
||||
尾递归优化过的 Fibonacci 数列实现如下。
|
||||
|
||||
```javascript
|
||||
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
|
||||
@@ -1375,11 +1374,11 @@ Fibonacci2(1000) // 7.0330367711422765e+208
|
||||
Fibonacci2(10000) // Infinity
|
||||
```
|
||||
|
||||
由此可见,“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6也是如此,第一次明确规定,所有ECMAScript的实现,都必须部署“尾调用优化”。这就是说,在ES6中,只要使用尾递归,就不会发生栈溢出,相对节省内存。
|
||||
由此可见,“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6 是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”。这就是说,ES6 中只要使用尾递归,就不会发生栈溢出,相对节省内存。
|
||||
|
||||
### 递归函数的改写
|
||||
|
||||
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量 total ,那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观,第一眼很难看出来,为什么计算5的阶乘,需要传入两个参数5和1?
|
||||
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量`total`,那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观,第一眼很难看出来,为什么计算`5`的阶乘,需要传入两个参数`5`和`1`?
|
||||
|
||||
两个方法可以解决这个问题。方法一是在尾递归函数之外,再提供一个正常形式的函数。
|
||||
|
||||
@@ -1396,7 +1395,7 @@ function factorial(n) {
|
||||
factorial(5) // 120
|
||||
```
|
||||
|
||||
上面代码通过一个正常形式的阶乘函数 factorial ,调用尾递归函数 tailFactorial ,看起来就正常多了。
|
||||
上面代码通过一个正常形式的阶乘函数`factorial`,调用尾递归函数`tailFactorial`,看起来就正常多了。
|
||||
|
||||
函数式编程有一个概念,叫做柯里化(currying),意思是将多参数的函数转换成单参数的形式。这里也可以使用柯里化。
|
||||
|
||||
@@ -1417,9 +1416,9 @@ const factorial = currying(tailFactorial, 1);
|
||||
factorial(5) // 120
|
||||
```
|
||||
|
||||
上面代码通过柯里化,将尾递归函数 tailFactorial 变为只接受1个参数的 factorial 。
|
||||
上面代码通过柯里化,将尾递归函数`tailFactorial`变为只接受一个参数的`factorial`。
|
||||
|
||||
第二种方法就简单多了,就是采用ES6的函数默认值。
|
||||
第二种方法就简单多了,就是采用 ES6 的函数默认值。
|
||||
|
||||
```javascript
|
||||
function factorial(n, total = 1) {
|
||||
@@ -1430,13 +1429,13 @@ function factorial(n, total = 1) {
|
||||
factorial(5) // 120
|
||||
```
|
||||
|
||||
上面代码中,参数 total 有默认值1,所以调用时不用提供这个值。
|
||||
上面代码中,参数`total`有默认值`1`,所以调用时不用提供这个值。
|
||||
|
||||
总结一下,递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言极其重要。对于其他支持“尾调用优化”的语言(比如Lua,ES6),只需要知道循环可以用递归代替,而一旦使用递归,就最好使用尾递归。
|
||||
|
||||
### 严格模式
|
||||
|
||||
ES6的尾调用优化只在严格模式下开启,正常模式是无效的。
|
||||
ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。
|
||||
|
||||
这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。
|
||||
|
||||
@@ -1447,7 +1446,7 @@ ES6的尾调用优化只在严格模式下开启,正常模式是无效的。
|
||||
|
||||
```javascript
|
||||
function restricted() {
|
||||
"use strict";
|
||||
'use strict';
|
||||
restricted.caller; // 报错
|
||||
restricted.arguments; // 报错
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user