## 三、函数

> 原文：[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 万个命令。而且，可用的词汇的定义往往比人类语言更精确，因此灵活性更低。 因此，我们通常会引入新的概念，来避免过多重复。

## 定义函数

函数定义是一个常规绑定，其中绑定的值是一个函数。 例如，这段代码定义了`square`，来引用一个函数，它产生给定数字的平方：

```js
const square = function(x) {
  return x * x;
};

console.log(square(12));
// → 144
```

函数使用以关键字`function`起始的表达式创建。 函数有一组参数（在本例中只有`x`）和一个主体，它包含调用该函数时要执行的语句。 以这种方式创建的函数的函数体，必须始终包在花括号中，即使它仅包含一个语句。

一个函数可以包含多个参数，也可以不含参数。在下面的例子中，`makeNoise`函数中没有包含任何参数，而`power`则使用了两个参数：

```js
var makeNoise = function() {
  console.log("Pling!");
};

makeNoise();
// → Pling!

const power = function(base, exponent) {
  let result = 1;
  for (let count = 0; count < exponent; count++) {
     result *= base;
  }
  return result;
};

console.log(power(2, 10));
// → 1024
```

有些函数会产生一个值，比如`power`和`square`，有些函数不会，比如`makeNoise`，它的唯一结果是副作用。 `return`语句决定函数返回的值。 当控制流遇到这样的语句时，它立即跳出当前函数并将返回的值赋给调用该函数的代码。 不带表达式的`return`关键字，会导致函数返回`undefined`。 没有`return`语句的函数，比如`makeNoise`，同样返回`undefined`。

函数的参数行为与常规绑定相似，但它们的初始值由函数的调用者提供，而不是函数本身的代码。

## 绑定和作用域

每个绑定都有一个作用域，它是程序的一部分，其中绑定是可见的。 对于在任何函数或块之外定义的绑定，作用域是整个程序 - 您可以在任何地方引用这种绑定。它们被称为全局的。

但是为函数参数创建的，或在函数内部声明的绑定，只能在该函数中引用，所以它们被称为局部绑定。 每次调用该函数时，都会创建这些绑定的新实例。 这提供了函数之间的一些隔离 - 每个函数调用，都在它自己的小世界（它的局部环境）中运行，并且通常可以在不知道全局环境中发生的事情的情况下理解。

用`let`和`const`声明的绑定，实际上是它们的声明所在的块的局部对象，所以如果你在循环中创建了一个，那么循环之前和之后的代码就不能“看见”它。JavaScript 2015 之前，只有函数创建新的作用域，因此，使用`var`关键字创建的旧式绑定，在它们出现的整个函数中内都可见，或者如果它们不在函数中，在全局作用域可见。

```js
let x = 10;
if (true) {
  let y = 20;
  var z = 30;
  console.log(x + y + z);
  // → 60
}
// y is not visible here
console.log(x + z);
// → 40
```

每个作用域都可以“向外查看”它周围的作用域，所以示例中的块内可以看到`x`。 当多个绑定具有相同名称时例外 - 在这种情况下，代码只能看到最内层的那个。 例如，当`halve`函数中的代码引用`n`时，它看到它自己的`n`，而不是全局的`n`。

```js
const halve = function(n) {
  return n / 2;
}
let n = 10;
console.log(halve(100));
// → 50
console.log(n);
// → 10
```

## 嵌套作用域

JavaScript 不仅区分全局和局部绑定。 块和函数可以在其他块和函数内部创建，产生多层局部环境。

例如，这个函数（输出制作一批鹰嘴豆泥所需的配料）的内部有另一个函数：

```js
const hummus = function(factor) {
  const ingredient = function(amount, unit, name) {
    let ingredientAmount = amount * factor;
    if (ingredientAmount > 1) {
      unit += "s";
    }
    console.log(`${ingredientAmount} ${unit} ${name}`);
  };
  ingredient(1, "can", "chickpeas");
  ingredient(0.25, "cup", "tahini");
  ingredient(0.25, "cup", "lemon juice");
  ingredient(1, "clove", "garlic");
  ingredient(2, "tablespoon", "olive oil");
  ingredient(0.5, "teaspoon", "cumin");
};
```

`ingredient`函数中的代码，可以从外部函数中看到`factor`绑定。 但是它的局部绑定，比如`unit`或`ingredientAmount`，在外层函数中是不可见的。

简而言之，每个局部作用域也可以看到所有包含它的局部作用域。 块内可见的绑定集，由这个块在程序文本中的位置决定。 每个局部作用域也可以看到包含它的所有局部作用域，并且所有作用域都可以看到全局作用域。 这种绑定可见性方法称为词法作用域。

## 作为值的函数

函数绑定通常只充当程序特定部分的名称。 这样的绑定被定义一次，永远不会改变。 这使得容易混淆函数和名称。

```js
let launchMissiles = function(value) {
  missileSystem.launch("now");
};
if (safeMode) {
  launchMissiles = function() {/* do nothing */};
}
```

在第 5 章中，我们将会讨论一些高级功能：将函数类型的值传递给其他函数。

## 符号声明

创建函数绑定的方法稍短。 当在语句开头使用`function`关键字时，它的工作方式不同。

```js
function square(x) {
  return x * x;
}
```

这是函数声明。 该语句定义了绑定`square`并将其指向给定的函数。 写起来稍微容易一些，并且在函数之后不需要分号。

这种形式的函数定义有一个微妙之处。

```js
console.log("The future says:", future());

function future() {
  return "You'll never have flying cars";
}
```

前面的代码可以执行，即使在函数定义在使用它的代码下面。 函数声明不是常规的从上到下的控制流的一部分。 在概念上，它们移到了其作用域的顶部，并可被该作用域内的所有代码使用。 这有时是有用的，因为它以一种看似有意义的方式，提供了对代码进行排序的自由，而无需担心在使用之前必须定义所有函数。

## 箭头函数

函数的第三个符号与其他函数看起来有很大不同。 它不使用`function`关键字，而是使用由等号和大于号组成的箭头（`=>`）（不要与大于等于运算符混淆，该运算符写做`>=`）。

```js
const power = (base, exponent) => {
  let result = 1;
  for (let count = 0; count < exponent; count++) {
    result *= base;
  }
  return result;
};
```

箭头出现在参数列表后面，然后是函数的主体。 它表达了一些东西，类似“这个输入（参数）产生这个结果（主体）”。

如果只有一个参数名称，则可以省略参数列表周围的括号。 如果主体是单个表达式，而不是大括号中的块，则表达式将从函数返回。 所以这两个`square`的定义是一样的：

```js
const square1 = (x) => { return x * x; };
const square2 = x => x * x;
```

当一个箭头函数没有参数时，它的参数列表只是一组空括号。

```js
const horn = () => {
  console.log("Toot");
};
```

在语言中没有很好的理由，同时拥有箭头函数和函数表达式。 除了我们将在第 6 章中讨论的一个小细节外，他们实现相同的东西。 在 2015 年增加了箭头函数，主要是为了能够以简短的方式编写小函数表达式。 我们将在第 5 章中使用它们。

## 调用栈

控制流经过函数的方式有点复杂。 让我们仔细看看它。 这是一个简单的程序，它执行了一些函数调用：

```js
function greet(who) {
  console.log("Hello " + who);
}
greet("Harry");
console.log("Bye");
```

这个程序的执行大致是这样的：对`greet`的调用使控制流跳转到该函数的开始（第 2 行）。 该函数调用控制台的`console.log`来完成它的工作，然后将控制流返回到第 2 行。 它到达`greet`函数的末尾，所以它返回到调用它的地方，这是第 4 行。 之后的一行再次调用`console.log`。 之后，程序结束。

我们可以使用下图表示出控制流：

```
not in function
   in greet
        in console.log
   in greet
not in function
   in console.log
not in function
```

由于函数在返回时必须跳回调用它的地方，因此计算机必须记住调用发生处上下文。 在一种情况下，`console.log`完成后必须返回`greet`函数。 在另一种情况下，它返回到程序的结尾。

计算机存储此上下文的地方是调用栈。 每次调用函数时，当前上下文都存储在此栈的顶部。 当函数返回时，它会从栈中删除顶部上下文，并使用该上下文继续执行。

存储这个栈需要计算机内存中的空间。 当栈变得太大时，计算机将失败，并显示“栈空间不足”或“递归太多”等消息。 下面的代码通过向计算机提出一个非常困难的问题来说明这一点，这个问题会导致两个函数之间的无限的来回调用。 相反，如果计算机有无限的栈，它将会是无限的。 事实上，我们将耗尽空间，或者“把栈顶破”。

```js
function chicken() {
  return egg();
}
function egg() {
  return chicken();
}
console.log(chicken() + " came first.");
// → ??
```

## 可选参数

下面的代码可以正常执行：

```js
function square(x) { return x * x; }
console.log(square(4, true, "hedgehog"));
// → 16
```

我们定义了`square`，只带有一个参数。 然而，当我们使用三个参数调用它时，语言并不会报错。 它会忽略额外的参数并计算第一个参数的平方。

JavaScript 对传入函数的参数数量几乎不做任何限制。如果你传递了过多参数，多余的参数就会被忽略掉，而如果你传递的参数过少，遗漏的参数将会被赋值成`undefined`。

该特性的缺点是你可能恰好向函数传递了错误数量的参数，但没有人会告诉你这个错误。

优点是这种行为可以用于使用不同数量的参数调用一个函数。 例如，这个`minus`函数试图通过作用于一个或两个参数，来模仿`-`运算符：

```js
function minus(a, b) {
  if (b === undefined) return -a;
  else return a - b;
}

console.log(minus(10));
// → -10
console.log(minus(10, 5));
// → 5
```

如果你在一个参数后面写了一个`=`运算符，然后是一个表达式，那么当没有提供它时，该表达式的值将会替换该参数。

例如，这个版本的`power`使其第二个参数是可选的。 如果你没有提供或传递`undefined`，它将默认为 2，函数的行为就像`square`。

```js
function power(base, exponent = 2) {
  let result = 1;
  for (let count = 0; count < exponent; count++) {
    result *= base;
  }
  return result;
}

console.log(power(4));
// → 16
console.log(power(2, 6));
// → 64
```

在下一章当中，我们将会了解如何获取传递给函数的整个参数列表。我们可以借助于这种特性来实现函数接收任意数量的参数。比如`console.log`就利用了这种特性，它可以用来输出所有传递给它的值。

```js
console.log("C", "O", 2);
// → C O 2
```

## 闭包

函数可以作为值使用，而且其局部绑定会在每次函数调用时重新创建，由此引出一个值得我们探讨的问题：如果函数已经执行结束，那么这些由函数创建的局部绑定会如何处理呢？

下面的示例代码展示了这种情况。代码中定义了函数`wrapValue`，该函数创建了一个局部绑定`localVariable`，并返回一个函数，用于访问并返回局部绑定`localVariable`。

```js
function wrapValue(n) {
  let local = n;
  return () => local;
}

let wrap1 = wrapValue(1);
let wrap2 = wrapValue(2);
console.log(wrap1());
// → 1
console.log(wrap2());
// → 2
```

这是允许的并且按照您的希望运行 - 绑定的两个实例仍然可以访问。 这种情况很好地证明了一个事实，每次调用都会重新创建局部绑定，而且不同的调用不能覆盖彼此的局部绑定。

这种特性（可以引用封闭作用域中的局部绑定的特定实例）称为闭包。 引用来自周围的局部作用域的绑定的函数称为（一个）闭包。 这种行为不仅可以让您免于担心绑定的生命周期，而且还可以以创造性的方式使用函数值。

我们对上面那个例子稍加修改，就可以创建一个可以乘以任意数字的函数。

```js
function multiplier(factor) {
 return number => number * factor;
}

let twice = multiplier(2);
console.log(twice(5));
// → 10
```

由于参数本身就是一个局部绑定，所以`wrapValue`示例中显式的`local`绑定并不是真的需要。

考虑这样的程序需要一些实践。 一个好的心智模型是，将函数值看作值，包含他们主体中的代码和它们的创建环境。 被调用时，函数体会看到它的创建环境，而不是它的调用环境。

这个例子调用`multiplier`并创建一个环境，其中`factor`参数绑定了 2。 它返回的函数值，存储在`twice`中，会记住这个环境。 所以当它被调用时，它将它的参数乘以 2。

## 递归

一个函数调用自己是完全可以的，只要它没有经常这样做以致溢出栈。 调用自己的函数被称为递归函数。 递归允许一些函数以不同的风格编写。 举个例子，这是`power`的替代实现：

```js
function power(base, exponent) {
  if (exponent == 0) {
    return 1;
  } else {
    return base * power(base, exponent - 1);
  }
}

console.log(power(2, 3));
// → 8
```

这与数学家定义幂运算的方式非常接近，并且可以比循环变体将该概念描述得更清楚。 该函数以更小的指数多次调用自己以实现重复的乘法。

但是这个实现有一个问题：在典型的 JavaScript 实现中，它大约比循环版本慢三倍。 通过简单循环来运行，通常比多次调用函数开销低。

速度与优雅的困境是一个有趣的问题。 您可以将其视为人性化和机器友好性之间的权衡。 几乎所有的程序都可以通过更大更复杂的方式加速。 程序员必须达到适当的平衡。

在`power`函数的情况下，不雅的（循环）版本仍然非常简单易读。 用递归版本替换它没有什么意义。 然而，通常情况下，一个程序处理相当复杂的概念，为了让程序更直接，放弃一些效率是有帮助的。

担心效率可能会令人分心。 这又是另一个让程序设计变复杂的因素，当你做了一件已经很困难的事情时，担心的额外事情可能会瘫痪。

因此，总是先写一些正确且容易理解的东西。 如果您担心速度太慢 - 通常不是这样，因为大多数代码的执行不足以花费大量时间 - 您可以事后进行测量并在必要时进行改进。

递归并不总是循环的低效率替代方法。 递归比循环更容易解决解决一些问题。 这些问题通常是需要探索或处理几个“分支”的问题，每个“分支”可能再次派生为更多的分支。

考虑这个难题：从数字 1 开始，反复加 5 或乘 3，就可以产生无限数量的新数字。 你会如何编写一个函数，给定一个数字，它试图找出产生这个数字的，这种加法和乘法的序列？

例如，数字 13 可以通过先乘 3 然后再加 5 两次来到达，而数字 15 根本无法到达。

使用递归编码的解决方案如下所示：

```js
function findSolution(target) {
  function find(current, history) {
    if (current == target) {
      return history;
    } else if (current > target) {
      return null;
    } else {
      return find(current + 5, `(${history} + 5)`) ||
             find(current * 3, `(${history} * 3)`);
    }
  }
  return find(1, "1");
}

console.log(findSolution(24));
// → (((1 * 3) + 5) * 3)
```

需要注意的是该程序并不需要找出最短运算序列，只需要找出任何一个满足要求的序列即可。

如果你没有看到它的工作原理，那也没关系。 让我们浏览它，因为它是递归思维的很好的练习。

内层函数`find`进行实际的递归。 它有两个参数：当前数字和记录我们如何到达这个数字的字符串。 如果找到解决方案，它会返回一个字符串，显示如何到达目标。 如果从这个数字开始找不到解决方案，则返回`null`。

为此，该函数执行三个操作之一。 如果当前数字是目标数字，则当前历史记录是到达目标的一种方式，因此将其返回。 如果当前的数字大于目标，则进一步探索该分支是没有意义的，因为加法和乘法只会使数字变大，所以它返回`null`。 最后，如果我们仍然低于目标数字，函数会尝试从当前数字开始的两个可能路径，通过调用它自己两次，一次是加法，一次是乘法。 如果第一次调用返回非`null`的东西，则返回它。 否则，返回第二个调用，无论它产生字符串还是`null`。

为了更好地理解函数执行过程，让我们来看一下搜索数字 13 时，`find`函数的调用情况：

```
find(1, "1")
  find(6, "(1 + 5)")
    find(11, "((1 + 5) + 5)")
      find(16, "(((1 + 5) + 5) + 5)")
        too big
      find(33, "(((1 + 5) + 5) * 3)")
        too big
    find(18, "((1 + 5) * 3)")
      too big
  find(3, "(1 * 3)")
    find(8, "((1 * 3) + 5)")
      find(13, "(((1 * 3) + 5) + 5)")
        found!
```

缩进表示调用栈的深度。 第一次调用`find`时，它首先调用自己来探索以`(1 + 5)`开始的解决方案。 这一调用将进一步递归，来探索每个后续的解，它产生小于或等于目标数字。 由于它没有找到一个命中目标的解，所以它向第一个调用返回`null`。 那里的`||`操作符会使探索`(1 * 3)`的调用发生。 这个搜索的运气更好 - 它的第一次递归调用，通过另一个递归调用，命中了目标数字。 最内层的调用返回一个字符串，并且中间调用中的每个“||”运算符都会传递该字符串，最终返回解决方案。

## 添加新函数

这里有两种常用的方法，将函数引入到程序中。

首先是你发现自己写了很多次非常相似的代码。 我们最好不要这样做。 拥有更多的代码，意味着更多的错误空间，并且想要了解程序的人阅读更多资料。 所以我们选取重复的功能，为它找到一个好名字，并把它放到一个函数中。

第二种方法是，你发现你需要一些你还没有写的功能，这听起来像是它应该有自己的函数。 您将首先命名该函数，然后您将编写它的主体。 在实际定义函数本身之前，您甚至可能会开始编写使用该函数的代码。

给函数起名的难易程度取决于我们封装的函数的用途是否明确。对此，我们一起来看一个例子。

我们想编写一个打印两个数字的程序，第一个数字是农场中牛的数量，第二个数字是农场中鸡的数量，并在数字后面跟上`Cows`和`Chickens`用以说明，并且在两个数字前填充 0，以使得每个数字总是由三位数字组成。

```
007 Cows
011 Chickens
```

这需要两个参数的函数 - 牛的数量和鸡的数量。 让我们来编程。

```js
function printFarmInventory(cows, chickens) {
  let cowString = String(cows);
  while (cowString.length < 3) {
    cowString = "0" + cowString;
  }
  console.log(`${cowString} Cows`);
  let chickenString = String(chickens);
  while (chickenString.length < 3) {
    chickenString = "0" + chickenString;
  }
  console.log(`${chickenString} Chickens`);
}
printFarmInventory(7, 11);
```

在字符串表达式后面写`.length`会给我们这个字符串的长度。 因此，`while`循环在数字字符串前面加上零，直到它们至少有三个字符的长度。

任务完成！ 但就在我们即将向农民发送代码（连同大量发票）时，她打电话告诉我们，她也开始饲养猪，我们是否可以扩展软件来打印猪的数量？

当然没有问题。但是当再次复制粘贴这四行代码的时候，我们停了下来并重新思考。一定还有更好的方案来解决我们的问题。以下是第一种尝试：

```js
function printZeroPaddedWithLabel(number, label) {
  let numberString = String(number);
  while (numberString.length < 3) {
    numberString = "0" + numberString;
  }
  console.log(`${numberString} ${label}`);
}

function printFarmInventory(cows, chickens, pigs) {
  printZeroPaddedWithLabel(cows, "Cows");
  printZeroPaddedWithLabel(chickens, "Chickens");
  printZeroPaddedWithLabel(pigs, "Pigs");
}

printFarmInventory(7, 11, 3);
```

这种方法解决了我们的问题！但是`printZeroPaddedWithLabel`这个函数并不十分恰当。它把三个操作，即打印信息、数字补零和添加标签放到了一个函数中处理。

这一次，我们不再将程序当中重复的代码提取成一个函数，而只是提取其中一项操作。

```js
function zeroPad(number, width) {
  let string = String(number);
  while (string.length < width) {
    string = "0" + string;
  }
  return string;
}

function printFarmInventory(cows, chickens, pigs) {
  console.log(`${zeroPad(cows, 3)} Cows`);
  console.log(`${zeroPad(chickens, 3)} Chickens`);
  console.log(`${zeroPad(pigs, 3)} Pigs`);
}

printFarmInventory(7, 16, 3);
```

名为`zeroPad`的函数具有很好的名称，使读取代码的人更容易弄清它的功能。 而且这样的函数在更多的情况下是有用的，不仅仅是这个特定程序。 例如，您可以使用它来帮助打印精确对齐的数字表格。

我们的函数应该包括多少功能呢？我们可以编写一个非常简单的函数，只支持将数字扩展成 3 字符宽。也可以编写一个复杂通用的数字格式化系统，可以处理分数、负数、小数点对齐和使用不同字符填充等。

一个实用原则是不要故作聪明，除非你确定你会需要它。 为你遇到的每一个功能编写通用“框架”是很诱人的。 控制住那种冲动。 你不会完成任何真正的工作 - 你只会编写你永远不会使用的代码。

## 函数及其副作用

我们可以将函数分成两类：一类调用后产生副作用，而另一类则产生返回值（当然我们也可以定义同时产生副作用和返回值的函数）。

在农场案例当中，我们调用第一个辅助函数`printZeroPaddedWithLabel`来产生副作用，打印一行文本信息。而在第二个版本中有一个`zeroPad`函数，我们调用它来产生返回值。第二个函数比第一个函数的应用场景更加广泛，这并非偶然。相比于直接产生副作用的函数，产生返回值的函数则更容易集成到新的环境当中使用。

纯函数是一种特定类型的，生成值的函数，它不仅没有副作用，而且也不依赖其他代码的副作用，例如，它不读取值可能会改变的全局绑定。 纯函数具有令人愉快的属性，当用相同的参数调用它时，它总是产生相同的值（并且不会做任何其他操作）。 这种函数的调用，可以由它的返回值代替而不改变代码的含义。 当你不确定纯函数是否正常工作时，你可以通过简单地调用它来测试它，并且知道如果它在当前上下文中工作，它将在任何上下文中工作。 非纯函数往往需要更多的脚手架来测试。

尽管如此，我们也没有必要觉得非纯函数就不好，然后将这类函数从代码中删除。副作用常常是非常有用的。比如说，我们不可能去编写一个纯函数版本的`console.log`，但`console.log`依然十分实用。而在副作用的帮助下，有些操作则更易、更快实现，因此考虑到运算速度，有时候纯函数并不可取。

## 本章小结

本章教你如何编写自己的函数。 当用作表达式时，`function`关键字可以创建一个函数值。 当作为一个语句使用时，它可以用来声明一个绑定，并给它一个函数作为它的值。 箭头函数是另一种创建函数的方式。

```js
// Define f to hold a function value
const f = function(a) {
  console.log(a + 2);
};

// Declare g to be a function
function g(a, b) {
  return a * b * 3.5;
}

// A less verbose function value
let h = a => a % 3;
```

理解函数的一个关键方面是理解作用域。 每个块创建一个新的作用域。 在给定作用域内声明的参数和绑定是局部的，并且从外部看不到。 用`var`声明的绑定行为不同 - 它们最终在最近的函数作用域或全局作用域内。

将程序执行的任务分成不同的功能是有帮助的。 你不必重复自己，函数可以通过将代码分组成一些具体事物，来组织程序。

## 习题

### 最小值

前一章介绍了标准函数`Math.min`，它可以返回参数中的最小值。我们现在可以构建相似的东西。编写一个函数`min`，接受两个参数，并返回其最小值。

```js
// Your code here.

console.log(min(0, 10));
// → 0
console.log(min(0, -10));
// → -10
```

### 递归

我们已经看到，`%`（取余运算符）可以用于判断一个数是否是偶数，通过使用`% 2`来检查它是否被 2 整除。这里有另一种方法来判断一个数字是偶数还是奇数：

+   0是偶数

+   1是奇数

+   对于其他任何数字N，其奇偶性与N–2相同。

定义对应此描述的递归函数`isEven`。 该函数应该接受一个参数（一个正整数）并返回一个布尔值。

使用 50 与 75 测试该函数。想想如果参数为 –1 会发生什么以及产生相应结果的原因。请你想一个方法来修正该问题。

```js
// Your code here.

console.log(isEven(50));
// → true
console.log(isEven(75));
// → false
console.log(isEven(-1));
// → ??
```

### 字符计数

你可以通过编写`"string"[N]`，来从字符串中得到第`N`个字符或字母。 返回的值将是只包含一个字符的字符串（例如`"b"`）。 第一个字符的位置为零，这会使最后一个字符在`string.length - 1`。 换句话说，含有两个字符的字符串的长度为2，其字符的位置为 0 和 1。
 
编写一个函数`countBs`，接受一个字符串参数，并返回一个数字，表示该字符串中有多少个大写字母`"B"`。

接着编写一个函数`countChar`，和`countBs`作用一样，唯一区别是接受第二个参数，指定需要统计的字符（而不仅仅能统计大写字母`"B"`）。并使用这个新函数重写函数`countBs`。

```js
// Your code here.

console.log(countBs("BBC"));
// → 2
console.log(countChar("kakkerlak", "k"));
// → 4
```
