## 三、函數
> 原文:[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/)
> 人們認為計算機科學是天才的藝術,但是實際情況相反,只是許多人在其它人基礎上做一些東西,就像一面由石子壘成的墻。
>
> 高德納

函數是 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
```