1
0
mirror of https://github.com/apachecn/eloquent-js-3e-zh.git synced 2025-05-23 20:02:20 +00:00
This commit is contained in:
wizardforcel 2018-04-30 22:39:07 +08:00
parent 0cb890829d
commit 78e572cdf6
2 changed files with 71 additions and 59 deletions

130
3.md
View File

@ -1,16 +1,28 @@
## 三、函数
> 原文:[Functions](https://eloquentjavascript.net/03_functions.html)
>
> 译者:[飞龙](https://github.com/wizardforcel)
>
> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
>
> 自豪地采用[谷歌翻译](https://translate.google.cn/)
>
> 部分参考了[《JavaScript 编程精解(第 2 版)》](https://book.douban.com/subject/26707144/)
> 人们认为计算机科学是天才的艺术,但是实际情况相反,只是许多人在其它人基础上做一些东西,就像一面由石子垒成的墙。
>
> 高德纳
![](img/3-0.jpg)
函数是 JavaScript 编程的面包和黄油。 将一段程序包装成值的概念有很多用途。 它为我们提供了方法,用于构建更大程序,减少重复,将名称和子程序关联,以及将这些子程序相互隔离。
函数最明显的应用是定义新词汇。 用散文创造新词汇通常是不好的风格。 但在编程中,它是不可或缺的。
以英语为母语的典型成年人,大约有 2 万字的词汇量。 很少有编程语言内置了 2 万个命令。而且,可用的词汇的定义往往比人类语言更精确,因此灵活性更低。 因此,我们通常会引入新的概念,来避免过多重复。
### 3.1 定义函数
## 定义函数
函数定义是一个常规绑定,其中绑定的值是一个函数。 例如,这段代码定义了`square`,来引用一个函数,它产生给定数字的平方:
@ -25,9 +37,9 @@ console.log(square(12));
函数使用以关键字`function`起始的表达式创建。 函数有一组参数(在本例中只有`x`)和一个主体,它包含调用该函数时要执行的语句。 以这种方式创建的函数的函数体,必须始终包在花括号中,即使它仅包含一个语句。
一个函数可以包含多个参数也可以不含参数。在下面的例子中makeNoise函数中没有包含任何参数而power则使用了两个参数
一个函数可以包含多个参数,也可以不含参数。在下面的例子中,`makeNoise`函数中没有包含任何参数,而`power`则使用了两个参数:
```
```js
var makeNoise = function() {
console.log("Pling!");
};
@ -59,7 +71,7 @@ console.log(power(2, 10));
`let``const`声明的绑定实际上是它们的声明所在的块的局部对象所以如果你在循环中创建了一个那么循环之前和之后的代码就不能“看见”它。JavaScript 2015 之前,只有函数创建新的作用域,因此,使用`var`关键字创建的旧式绑定,在它们出现的整个函数中内都可见,或者如果它们不在函数中,在全局作用域可见。
```
```js
let x = 10;
if (true) {
let y = 20;
@ -74,7 +86,7 @@ console.log(x + z);
每个作用域都可以“向外查看”它周围的作用域,所以示例中的块内可以看到`x`。 当多个绑定具有相同名称时例外 - 在这种情况下,代码只能看到最内层的那个。 例如,当`halve`函数中的代码引用`n`时,它看到它自己的`n`,而不是全局的`n`
```
```js
const halve = function(n) {
return n / 2;
@ -85,13 +97,13 @@ console.log(n);
// → 10
```
### 嵌套作用域
## 嵌套作用域
JavaScript不仅区分全局和局部绑定。 块和函数可以在其他块和函数内部创建,产生多层局部环境。
JavaScript 不仅区分全局和局部绑定。 块和函数可以在其他块和函数内部创建,产生多层局部环境。
例如,这个函数(输出制作一批鹰嘴豆泥所需的配料)的内部有另一个函数:
```
```js
const hummus = function(factor) {
const ingredient = function(amount, unit, name) {
let ingredientAmount = amount * factor;
@ -113,11 +125,11 @@ const hummus = function(factor) {
简而言之,每个局部作用域也可以看到所有包含它的局部作用域。 块内可见的绑定集,由这个块在程序文本中的位置决定。 每个局部作用域也可以看到包含它的所有局部作用域,并且所有作用域都可以看到全局作用域。 这种绑定可见性方法称为词法作用域。
### 3.4 作为值的函数
## 作为值的函数
函数绑定通常只充当程序特定部分的名称。 这样的绑定被定义一次,永远不会改变。 这使得容易混淆函数和名称。
```
```js
let launchMissiles = function(value) {
missileSystem.launch("now");
};
@ -126,13 +138,13 @@ if (safeMode) {
}
```
在第5章中我们将会讨论一些高级功能将函数类型的值传递给其他函数。
在第 5 章中,我们将会讨论一些高级功能:将函数类型的值传递给其他函数。
### 3.5 符号声明
## 符号声明
创建函数绑定的方法稍短。 当在语句开头使用`function`关键字时,它的工作方式不同。
```
```js
function square(x) {
return x * x;
}
@ -142,7 +154,7 @@ function square(x) {
这种形式的函数定义有一个微妙之处。
```
```js
console.log("The future says:", future());
function future() {
@ -152,11 +164,11 @@ function future() {
前面的代码可以执行,即使在函数定义在使用它的代码下面。 函数声明不是常规的从上到下的控制流的一部分。 在概念上,它们移到了其作用域的顶部,并可被该作用域内的所有代码使用。 这有时是有用的,因为它以一种看似有意义的方式,提供了对代码进行排序的自由,而无需担心在使用之前必须定义所有函数。
### 箭头函数
## 箭头函数
函数的第三个符号与其他函数看起来有很大不同。 它不使用`function`关键字,而是使用由等号和大于号组成的箭头(`=>`)(不要与大于等于运算符混淆,该运算符写做`>=`)。
```
```js
const power = (base, exponent) => {
let result = 1;
for (let count = 0; count < exponent; count++) {
@ -170,14 +182,14 @@ const power = (base, exponent) => {
如果只有一个参数名称,则可以省略参数列表周围的括号。 如果主体是单个表达式,而不是大括号中的块,则表达式将从函数返回。 所以这两个`square`的定义是一样的:
```
```js
const square1 = (x) => { return x * x; };
const square2 = x => x * x;
```
当一个箭头函数没有参数时,它的参数列表只是一组空括号。
```
```js
const horn = () => {
console.log("Toot");
};
@ -185,11 +197,11 @@ const horn = () => {
在语言中没有很好的理由,同时拥有箭头函数和函数表达式。 除了我们将在第 6 章中讨论的一个小细节外,他们实现相同的东西。 在 2015 年增加了箭头函数,主要是为了能够以简短的方式编写小函数表达式。 我们将在第 5 章中使用它们。
### 3.6 调用栈
## 调用栈
控制流经过函数的方式有点复杂。 让我们仔细看看它。 这是一个简单的程序,它执行了一些函数调用:
```
```js
function greet(who) {
console.log("Hello " + who);
}
@ -217,7 +229,7 @@ not in function
存储这个栈需要计算机内存中的空间。 当栈变得太大时,计算机将失败,并显示“栈空间不足”或“递归太多”等消息。 下面的代码通过向计算机提出一个非常困难的问题来说明这一点,这个问题会导致两个函数之间的无限的来回调用。 相反,如果计算机有无限的栈,它将会是无限的。 事实上,我们将耗尽空间,或者“把栈顶破”。
```
```js
function chicken() {
return egg();
}
@ -228,11 +240,11 @@ console.log(chicken() + " came first.");
// → ??
```
### 3.7 可选参数
## 可选参数
下面的代码可以正常执行:
```
```js
function square(x) { return x * x; }
console.log(square(4, true, "hedgehog"));
// → 16
@ -240,13 +252,13 @@ console.log(square(4, true, "hedgehog"));
我们定义了`square`,只带有一个参数。 然而,当我们使用三个参数调用它时,语言并不会报错。 它会忽略额外的参数并计算第一个参数的平方。
JavaScript对传入函数的参数数量几乎不做任何限制。如果你传递了过多参数多余的参数就会被忽略掉而如果你传递的参数过少遗漏的参数将会被赋值成`undefined`
JavaScript 对传入函数的参数数量几乎不做任何限制。如果你传递了过多参数,多余的参数就会被忽略掉,而如果你传递的参数过少,遗漏的参数将会被赋值成`undefined`
该特性的缺点是你可能恰好向函数传递了错误数量的参数,但没有人会告诉你这个错误。
优点是这种行为可以用于使用不同数量的参数调用一个函数。 例如,这个`minus`函数试图通过作用于一个或两个参数,来模仿`-`运算符:
```
```js
function minus(a, b) {
if (b === undefined) return -a;
else return a - b;
@ -262,7 +274,7 @@ console.log(minus(10, 5));
例如,这个版本的`power`使其第二个参数是可选的。 如果你没有提供或传递`undefined`,它将默认为 2函数的行为就像`square`
```
```js
function power(base, exponent = 2) {
let result = 1;
for (let count = 0; count < exponent; count++) {
@ -277,20 +289,20 @@ console.log(power(2, 6));
// → 64
```
在下一章当中我们将会了解如何获取传递给函数的整个参数列表。我们可以借助于这种特性来实现函数接收任意数量的参数。比如console.log就利用了这种特性它可以用来输出所有传递给它的值。
在下一章当中,我们将会了解如何获取传递给函数的整个参数列表。我们可以借助于这种特性来实现函数接收任意数量的参数。比如`console.log`就利用了这种特性,它可以用来输出所有传递给它的值。
```
```js
console.log("C", "O", 2);
// → C O 2
```
### 3.8 闭包
## 闭包
函数可以作为值使用,而且其局部绑定会在每次函数调用时重新创建,由此引出一个值得我们探讨的问题:如果函数已经执行结束,那么这些由函数创建的局部绑定会如何处理呢?
下面的示例代码展示了这种情况。代码中定义了函数wrapValue该函数创建了一个局部绑定localVariable并返回一个函数用于访问并返回局部绑定localVariable。
下面的示例代码展示了这种情况。代码中定义了函数`wrapValue`,该函数创建了一个局部绑定`localVariable`,并返回一个函数,用于访问并返回局部绑定`localVariable`
```
```js
function wrapValue(n) {
let local = n;
return () => local;
@ -310,7 +322,7 @@ console.log(wrap2());
我们对上面那个例子稍加修改,就可以创建一个可以乘以任意数字的函数。
```
```js
function multiplier(factor) {
return number => number * factor;
}
@ -326,11 +338,11 @@ console.log(twice(5));
这个例子调用`multiplier`并创建一个环境,其中`factor`参数绑定了 2。 它返回的函数值,存储在`twice`中,会记住这个环境。 所以当它被调用时,它将它的参数乘以 2。
### 3.9 递归
## 递归
一个函数自己调用是完全可以的,只要它没有经常这样做以致溢出栈。 调用自己的函数被称为递归函数。 递归允许一些函数以不同的风格编写。 举个例子,这是`power`的替代实现:
一个函数调用自己是完全可以的,只要它没有经常这样做以致溢出栈。 调用自己的函数被称为递归函数。 递归允许一些函数以不同的风格编写。 举个例子,这是`power`的替代实现:
```
```js
function power(base, exponent) {
if (exponent == 0) {
return 1;
@ -363,7 +375,7 @@ console.log(power(2, 3));
使用递归编码的解决方案如下所示:
```
```js
function findSolution(target) {
function find(current, history) {
if (current == target) {
@ -390,7 +402,7 @@ console.log(findSolution(24));
为此,该函数执行三个操作之一。 如果当前数字是目标数字,则当前历史记录是到达目标的一种方式,因此将其返回。 如果当前的数字大于目标,则进一步探索该分支是没有意义的,因为加法和乘法只会使数字变大,所以它返回`null`。 最后,如果我们仍然低于目标数字,函数会尝试从当前数字开始的两个可能路径,通过调用它自己两次,一次是加法,一次是乘法。 如果第一次调用返回非`null`的东西,则返回它。 否则,返回第二个调用,无论它产生字符串还是`null`
为了更好地理解函数执行过程让我们来看一下搜索数字13时find函数的调用情况
为了更好地理解函数执行过程,让我们来看一下搜索数字 13 时,`find`函数的调用情况:
```
find(1, "1")
@ -410,7 +422,7 @@ find(1, "1")
缩进表示调用栈的深度。 第一次调用`find`时,它首先调用自己来探索以`(1 + 5)`开始的解决方案。 这一调用将进一步递归,来探索每个后续的解,它产生小于或等于目标数字。 由于它没有找到一个命中目标的解,所以它向第一个调用返回`null`。 那里的`||`操作符会使探索`(1 * 3)`的调用发生。 这个搜索的运气更好 - 它的第一次递归调用,通过另一个递归调用,命中了目标数字。 最内层的调用返回一个字符串,并且中间调用中的每个“||”运算符都会传递该字符串,最终返回解决方案。
### 3.10 添加新函数
## 添加新函数
这里有两种常用的方法,将函数引入到程序中。
@ -420,7 +432,7 @@ find(1, "1")
给函数起名的难易程度取决于我们封装的函数的用途是否明确。对此,我们一起来看一个例子。
我们想编写一个打印两个数字的程序第一个数字是农场中牛的数量第二个数字是农场中鸡的数量并在数字后面跟上Cows和Chickens用以说明并且在两个数字前填充0以使得每个数字总是由三位数字组成。
我们想编写一个打印两个数字的程序,第一个数字是农场中牛的数量,第二个数字是农场中鸡的数量,并在数字后面跟上`Cows``Chickens`用以说明,并且在两个数字前填充 0以使得每个数字总是由三位数字组成。
```
007 Cows
@ -429,7 +441,7 @@ find(1, "1")
这需要两个参数的函数 - 牛的数量和鸡的数量。 让我们来编程。
```
```js
function printFarmInventory(cows, chickens) {
let cowString = String(cows);
while (cowString.length < 3) {
@ -451,7 +463,7 @@ printFarmInventory(7, 11);
当然没有问题。但是当再次复制粘贴这四行代码的时候,我们停了下来并重新思考。一定还有更好的方案来解决我们的问题。以下是第一种尝试:
```
```js
function printZeroPaddedWithLabel(number, label) {
let numberString = String(number);
while (numberString.length < 3) {
@ -469,11 +481,11 @@ function printFarmInventory(cows, chickens, pigs) {
printFarmInventory(7, 11, 3);
```
这种方法解决了我们的问题但是printZeroPaddedWithLabel这个函数并不十分恰当。它把三个操作即打印信息、数字补零和添加标签放到了一个函数中处理。
这种方法解决了我们的问题!但是`printZeroPaddedWithLabel`这个函数并不十分恰当。它把三个操作,即打印信息、数字补零和添加标签放到了一个函数中处理。
这一次,我们不再将程序当中重复的代码提取成一个函数,而只是提取其中一项操作。
```
```js
function zeroPad(number, width) {
let string = String(number);
while (string.length < width) {
@ -493,25 +505,25 @@ printFarmInventory(7, 16, 3);
名为`zeroPad`的函数具有很好的名称,使读取代码的人更容易弄清它的功能。 而且这样的函数在更多的情况下是有用的,不仅仅是这个特定程序。 例如,您可以使用它来帮助打印精确对齐的数字表格。
我们的函数应该包括多少功能呢我们可以编写一个非常简单的函数只支持将数字扩展成3字符宽。也可以编写一个复杂通用的数字格式化系统可以处理分数、负数、小数点对齐和使用不同字符填充等。
我们的函数应该包括多少功能呢?我们可以编写一个非常简单的函数,只支持将数字扩展成 3 字符宽。也可以编写一个复杂通用的数字格式化系统,可以处理分数、负数、小数点对齐和使用不同字符填充等。
一个实用原则是不要故作聪明,除非你确定你会需要它。 为你遇到的每一个功能编写通用“框架”是很诱人的。 控制住那种冲动。 你不会完成任何真正的工作 - 你只会编写你永远不会使用的代码。
### 3.11 函数及其副作用
## 函数及其副作用
我们可以将函数分成两类:一类调用后产生副作用,而另一类则产生返回值(当然我们也可以定义同时产生副作用和返回值的函数)。
在农场案例当中我们调用第一个辅助函数printZeroPaddedWithLabel来产生副作用打印一行文本信息。而在第二个版本中有一个zeroPad函数我们调用它来产生返回值。第二个函数比第一个函数的应用场景更加广泛这并非偶然。相比于直接产生副作用的函数产生返回值的函数则更容易集成到新的环境当中使用。
在农场案例当中,我们调用第一个辅助函数`printZeroPaddedWithLabel`来产生副作用,打印一行文本信息。而在第二个版本中有一个`zeroPad`函数,我们调用它来产生返回值。第二个函数比第一个函数的应用场景更加广泛,这并非偶然。相比于直接产生副作用的函数,产生返回值的函数则更容易集成到新的环境当中使用。
纯函数是一种特定类型的,生成值的函数,它不仅没有副作用,而且也不依赖其他代码的副作用,例如,它不读取值可能会改变的全局绑定。 纯函数具有令人愉快的属性,当用相同的参数调用它时,它总是产生相同的值(并且不会做任何其他操作)。 这种函数的调用,可以由它的返回值代替而不改变代码的含义。 当你不确定纯函数是否正常工作时,你可以通过简单地调用它来测试它,并且知道如果它在当前上下文中工作,它将在任何上下文中工作。 非纯函数往往需要更多的脚手架来测试。
尽管如此我们也没有必要觉得非纯函数就不好然后将这类函数从代码中删除。副作用常常是非常有用的。比如说我们不可能去编写一个纯函数版本的console.log但console.log依然十分实用。而在副作用的帮助下有些操作则更易、更快实现因此考虑到运算速度有时候纯函数并不可取。
尽管如此,我们也没有必要觉得非纯函数就不好,然后将这类函数从代码中删除。副作用常常是非常有用的。比如说,我们不可能去编写一个纯函数版本的`console.log`,但`console.log`依然十分实用。而在副作用的帮助下,有些操作则更易、更快实现,因此考虑到运算速度,有时候纯函数并不可取。
### 3.12 本章小结
## 本章小结
本章教你如何编写自己的函数。 当用作表达式时,`function`关键字可以创建一个函数值。 当作为一个语句使用时,它可以用来声明一个绑定,并给它一个函数作为它的值。 箭头函数是另一种创建函数的方式。
```
```js
// Define f to hold a function value
const f = function(a) {
console.log(a + 2);
@ -530,13 +542,13 @@ let h = a => a % 3;
将程序执行的任务分成不同的功能是有帮助的。 你不必重复自己,函数可以通过将代码分组成一些具体事物,来组织程序。
### 3.13 习题
## 习题
#### 3.13.1 最小值
### 最小值
前一章介绍了标准函数Math.min它可以返回参数中的最小值。我们现在可以构建相似的东西。编写一个函数min接受两个参数并返回其最小值。
前一章介绍了标准函数`Math.min`,它可以返回参数中的最小值。我们现在可以构建相似的东西。编写一个函数`min`,接受两个参数,并返回其最小值。
```
```js
// Your code here.
console.log(min(0, 10));
@ -545,7 +557,7 @@ console.log(min(0, -10));
// → -10
```
#### 3.13.2 递归
### 递归
我们已经看到,`%`(取余运算符)可以用于判断一个数是否是偶数,通过使用`% 2`来检查它是否被 2 整除。这里有另一种方法来判断一个数字是偶数还是奇数:
@ -559,7 +571,7 @@ console.log(min(0, -10));
使用 50 与 75 测试该函数。想想如果参数为 1 会发生什么以及产生相应结果的原因。请你想一个方法来修正该问题。
```
```js
// Your code here.
console.log(isEven(50));
@ -570,15 +582,15 @@ console.log(isEven(-1));
// → ??
```
#### 3.13.3 字符计数
### 3.13.3 字符计数
你可以通过编写`"string"[N]`,来从字符串中得到第`N`个字符或字母。 返回的值将是只包含一个字符的字符串(例如`"b"`)。 第一个字符的位置为零,这会使最后一个字符在`string.length - 1`。 换句话说含有两个字符的字符串的长度为2其字符的位置为 0 和 1。
编写一个函数countBs接受一个字符串参数并返回一个数字表示该字符串中有多少个大写字母“B”
编写一个函数`countBs`,接受一个字符串参数,并返回一个数字,表示该字符串中有多少个大写字母`"B"`
接着编写一个函数countChar和countBs作用一样唯一区别是接受第二个参数指定需要统计的字符而不仅仅能统计大写字母“B”。并使用这个新函数重写函数countBs
接着编写一个函数`countChar`,和`countBs`作用一样,唯一区别是接受第二个参数,指定需要统计的字符(而不仅仅能统计大写字母`"B"`)。并使用这个新函数重写函数`countBs`
```
```js
// Your code here.
console.log(countBs("BBC"));

BIN
img/3-0.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB