來源: ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Functionsjavascript
譯者:飛龍html
協議:CC BY-NC-SA 4.0java
自豪地採用谷歌翻譯git
部分參考了《JavaScript 編程精解(第 2 版)》程序員
人們認爲計算機科學是天才的藝術,可是實際狀況相反,只是許多人在其它人基礎上作一些東西,就像一面由石子壘成的牆。github
高德納apache
函數是 JavaScript 編程的麪包和黃油。 將一段程序包裝成值的概念有不少用途。 它爲咱們提供了方法,用於構建更大程序,減小重複,將名稱和子程序關聯,以及將這些子程序相互隔離。編程
函數最明顯的應用是定義新詞彙。 用散文創造新詞彙一般是很差的風格。 但在編程中,它是不可或缺的。閉包
以英語爲母語的典型成年人,大約有 2 萬字的詞彙量。 不多有編程語言內置了 2 萬個命令。並且,可用的詞彙的定義每每比人類語言更精確,所以靈活性更低。 所以,咱們一般會引入新的概念,來避免過多重複。框架
函數定義是一個常規綁定,其中綁定的值是一個函數。 例如,這段代碼定義了square
,來引用一個函數,它產生給定數字的平方:
const square = function(x) { return x * x; }; console.log(square(12)); // → 144
函數使用以關鍵字function
起始的表達式建立。 函數有一組參數(在本例中只有x
)和一個主體,它包含調用該函數時要執行的語句。 以這種方式建立的函數的函數體,必須始終包在花括號中,即便它僅包含一個語句。
一個函數能夠包含多個參數,也能夠不含參數。在下面的例子中,makeNoise
函數中沒有包含任何參數,而power
則使用了兩個參數:
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
關鍵字建立的舊式綁定,在它們出現的整個函數中內均可見,或者若是它們不在函數中,在全局做用域可見。
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
。
const halve = function(n) { return n / 2; } let n = 10; console.log(halve(100)); // → 50 console.log(n); // → 10
JavaScript 不只區分全局和局部綁定。 塊和函數能夠在其餘塊和函數內部建立,產生多層局部環境。
例如,這個函數(輸出製做一批鷹嘴豆泥所需的配料)的內部有另外一個函數:
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
,在外層函數中是不可見的。
簡而言之,每一個局部做用域也能夠看到全部包含它的局部做用域。 塊內可見的綁定集,由這個塊在程序文本中的位置決定。 每一個局部做用域也能夠看到包含它的全部局部做用域,而且全部做用域均可以看到全局做用域。 這種綁定可見性方法稱爲詞法做用域。
函數綁定一般只充當程序特定部分的名稱。 這樣的綁定被定義一次,永遠不會改變。 這使得容易混淆函數和名稱。
let launchMissiles = function(value) { missileSystem.launch("now"); }; if (safeMode) { launchMissiles = function() {/* do nothing */}; }
在第 5 章中,咱們將會討論一些高級功能:將函數類型的值傳遞給其餘函數。
建立函數綁定的方法稍短。 當在語句開頭使用function
關鍵字時,它的工做方式不一樣。
function square(x) { return x * x; }
這是函數聲明。 該語句定義了綁定square
並將其指向給定的函數。 寫起來稍微容易一些,而且在函數以後不須要分號。
這種形式的函數定義有一個微妙之處。
console.log("The future says:", future()); function future() { return "You'll never have flying cars"; }
前面的代碼能夠執行,即便在函數定義在使用它的代碼下面。 函數聲明不是常規的從上到下的控制流的一部分。 在概念上,它們移到了其做用域的頂部,並可被該做用域內的全部代碼使用。 這有時是有用的,由於它以一種看似有意義的方式,提供了對代碼進行排序的自由,而無需擔憂在使用以前必須定義全部函數。
函數的第三個符號與其餘函數看起來有很大不一樣。 它不使用function
關鍵字,而是使用由等號和大於號組成的箭頭(=>
)(不要與大於等於運算符混淆,該運算符寫作>=
)。
const power = (base, exponent) => { let result = 1; for (let count = 0; count < exponent; count++) { result *= base; } return result; };
箭頭出如今參數列表後面,而後是函數的主體。 它表達了一些東西,相似「這個輸入(參數)產生這個結果(主體)」。
若是隻有一個參數名稱,則能夠省略參數列表周圍的括號。 若是主體是單個表達式,而不是大括號中的塊,則表達式將從函數返回。 因此這兩個square
的定義是同樣的:
const square1 = (x) => { return x * x; }; const square2 = x => x * x;
當一個箭頭函數沒有參數時,它的參數列表只是一組空括號。
const horn = () => { console.log("Toot"); };
在語言中沒有很好的理由,同時擁有箭頭函數和函數表達式。 除了咱們將在第 6 章中討論的一個小細節外,他們實現相同的東西。 在 2015 年增長了箭頭函數,主要是爲了可以以簡短的方式編寫小函數表達式。 咱們將在第 5 章中使用它們。
控制流通過函數的方式有點複雜。 讓咱們仔細看看它。 這是一個簡單的程序,它執行了一些函數調用:
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
函數。 在另外一種狀況下,它返回到程序的結尾。
計算機存儲此上下文的地方是調用棧。 每次調用函數時,當前上下文都存儲在此棧的頂部。 當函數返回時,它會從棧中刪除頂部上下文,並使用該上下文繼續執行。
存儲這個棧須要計算機內存中的空間。 當棧變得太大時,計算機將失敗,並顯示「棧空間不足」或「遞歸太多」等消息。 下面的代碼經過向計算機提出一個很是困難的問題來講明這一點,這個問題會致使兩個函數之間的無限的來回調用。 相反,若是計算機有無限的棧,它將會是無限的。 事實上,咱們將耗盡空間,或者「把棧頂破」。
function chicken() { return egg(); } function egg() { return chicken(); } console.log(chicken() + " came first."); // → ??
下面的代碼能夠正常執行:
function square(x) { return x * x; } console.log(square(4, true, "hedgehog")); // → 16
咱們定義了square
,只帶有一個參數。 然而,當咱們使用三個參數調用它時,語言並不會報錯。 它會忽略額外的參數並計算第一個參數的平方。
JavaScript 對傳入函數的參數數量幾乎不作任何限制。若是你傳遞了過多參數,多餘的參數就會被忽略掉,而若是你傳遞的參數過少,遺漏的參數將會被賦值成undefined
。
該特性的缺點是你可能剛好向函數傳遞了錯誤數量的參數,但沒有人會告訴你這個錯誤。
優勢是這種行爲能夠用於使用不一樣數量的參數調用一個函數。 例如,這個minus
函數試圖經過做用於一個或兩個參數,來模仿-
運算符:
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
。
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
就利用了這種特性,它能夠用來輸出全部傳遞給它的值。
console.log("C", "O", 2); // → C O 2
函數能夠做爲值使用,並且其局部綁定會在每次函數調用時從新建立,由此引出一個值得咱們探討的問題:若是函數已經執行結束,那麼這些由函數建立的局部綁定會如何處理呢?
下面的示例代碼展現了這種狀況。代碼中定義了函數wrapValue
,該函數建立了一個局部綁定localVariable
,並返回一個函數,用於訪問並返回局部綁定localVariable
。
function wrapValue(n) { let local = n; return () => local; } let wrap1 = wrapValue(1); let wrap2 = wrapValue(2); console.log(wrap1()); // → 1 console.log(wrap2()); // → 2
這是容許的而且按照您的但願運行 - 綁定的兩個實例仍然能夠訪問。 這種狀況很好地證實了一個事實,每次調用都會從新建立局部綁定,並且不一樣的調用不能覆蓋彼此的局部綁定。
這種特性(能夠引用封閉做用域中的局部綁定的特定實例)稱爲閉包。 引用來自周圍的局部做用域的綁定的函數稱爲(一個)閉包。 這種行爲不只可讓您免於擔憂綁定的生命週期,並且還能夠以創造性的方式使用函數值。
咱們對上面那個例子稍加修改,就能夠建立一個能夠乘以任意數字的函數。
function multiplier(factor) { return number => number * factor; } let twice = multiplier(2); console.log(twice(5)); // → 10
因爲參數自己就是一個局部綁定,因此wrapValue
示例中顯式的local
綁定並非真的須要。
考慮這樣的程序須要一些實踐。 一個好的心智模型是,將函數值看做值,包含他們主體中的代碼和它們的建立環境。 被調用時,函數體會看到它的建立環境,而不是它的調用環境。
這個例子調用multiplier
並建立一個環境,其中factor
參數綁定了 2。 它返回的函數值,存儲在twice
中,會記住這個環境。 因此當它被調用時,它將它的參數乘以 2。
一個函數調用本身是徹底能夠的,只要它沒有常常這樣作以至溢出棧。 調用本身的函數被稱爲遞歸函數。 遞歸容許一些函數以不一樣的風格編寫。 舉個例子,這是power
的替代實現:
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 根本沒法到達。
使用遞歸編碼的解決方案以下所示:
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
這須要兩個參數的函數 - 牛的數量和雞的數量。 讓咱們來編程。
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
循環在數字字符串前面加上零,直到它們至少有三個字符的長度。
任務完成! 但就在咱們即將向農民發送代碼(連同大量發票)時,她打電話告訴咱們,她也開始飼養豬,咱們是否能夠擴展軟件來打印豬的數量?
固然沒有問題。可是當再次複製粘貼這四行代碼的時候,咱們停了下來並從新思考。必定還有更好的方案來解決咱們的問題。如下是第一種嘗試:
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
這個函數並不十分恰當。它把三個操做,即打印信息、數字補零和添加標籤放到了一個函數中處理。
這一次,咱們再也不將程序當中重複的代碼提取成一個函數,而只是提取其中一項操做。
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
關鍵字能夠建立一個函數值。 看成爲一個語句使用時,它能夠用來聲明一個綁定,並給它一個函數做爲它的值。 箭頭函數是另外一種建立函數的方式。
// 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
,接受兩個參數,並返回其最小值。
// Your code here. console.log(min(0, 10)); // → 0 console.log(min(0, -10)); // → -10
咱們已經看到,%
(取餘運算符)能夠用於判斷一個數是不是偶數,經過使用% 2
來檢查它是否被 2 整除。這裏有另外一種方法來判斷一個數字是偶數仍是奇數:
定義對應此描述的遞歸函數isEven
。 該函數應該接受一個參數(一個正整數)並返回一個布爾值。
使用 50 與 75 測試該函數。想一想若是參數爲 –1 會發生什麼以及產生相應結果的緣由。請你想一個方法來修正該問題。
// 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
。
// Your code here. console.log(countBs("BBC")); // → 2 console.log(countChar("kakkerlak", "k")); // → 4