摘要:當(dāng)控制流遇到這樣的語句時(shí),它立即跳出當(dāng)前函數(shù)并將返回的值賦給調(diào)用該函數(shù)的代碼。函數(shù)聲明不是常規(guī)的從上到下的控制流的一部分。該函數(shù)調(diào)用控制臺(tái)的來完成它的工作,然后將控制流返回到第行。
來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:Functions
譯者:飛龍
協(xié)議:CC BY-NC-SA 4.0
自豪地采用谷歌翻譯
部分參考了《JavaScript 編程精解(第 2 版)》
人們認(rèn)為計(jì)算機(jī)科學(xué)是天才的藝術(shù),但是實(shí)際情況相反,只是許多人在其它人基礎(chǔ)上做一些東西,就像一面由石子壘成的墻。
高德納
函數(shù)是 JavaScript 編程的面包和黃油。 將一段程序包裝成值的概念有很多用途。 它為我們提供了方法,用于構(gòu)建更大程序,減少重復(fù),將名稱和子程序關(guān)聯(lián),以及將這些子程序相互隔離。
函數(shù)最明顯的應(yīng)用是定義新詞匯。 用散文創(chuàng)造新詞匯通常是不好的風(fēng)格。 但在編程中,它是不可或缺的。
以英語為母語的典型成年人,大約有 2 萬字的詞匯量。 很少有編程語言內(nèi)置了 2 萬個(gè)命令。而且,可用的詞匯的定義往往比人類語言更精確,因此靈活性更低。 因此,我們通常會(huì)引入新的概念,來避免過多重復(fù)。
定義函數(shù)函數(shù)定義是一個(gè)常規(guī)綁定,其中綁定的值是一個(gè)函數(shù)。 例如,這段代碼定義了square,來引用一個(gè)函數(shù),它產(chǎn)生給定數(shù)字的平方:
const square = function(x) { return x * x; }; console.log(square(12)); // → 144
函數(shù)使用以關(guān)鍵字function起始的表達(dá)式創(chuàng)建。 函數(shù)有一組參數(shù)(在本例中只有x)和一個(gè)主體,它包含調(diào)用該函數(shù)時(shí)要執(zhí)行的語句。 以這種方式創(chuàng)建的函數(shù)的函數(shù)體,必須始終包在花括號(hào)中,即使它僅包含一個(gè)語句。
一個(gè)函數(shù)可以包含多個(gè)參數(shù),也可以不含參數(shù)。在下面的例子中,makeNoise函數(shù)中沒有包含任何參數(shù),而power則使用了兩個(gè)參數(shù):
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
有些函數(shù)會(huì)產(chǎn)生一個(gè)值,比如power和square,有些函數(shù)不會(huì),比如makeNoise,它的唯一結(jié)果是副作用。 return語句決定函數(shù)返回的值。 當(dāng)控制流遇到這樣的語句時(shí),它立即跳出當(dāng)前函數(shù)并將返回的值賦給調(diào)用該函數(shù)的代碼。 不帶表達(dá)式的return關(guān)鍵字,會(huì)導(dǎo)致函數(shù)返回undefined。 沒有return語句的函數(shù),比如makeNoise,同樣返回undefined。
函數(shù)的參數(shù)行為與常規(guī)綁定相似,但它們的初始值由函數(shù)的調(diào)用者提供,而不是函數(shù)本身的代碼。
綁定和作用域每個(gè)綁定都有一個(gè)作用域,它是程序的一部分,其中綁定是可見的。 對于在任何函數(shù)或塊之外定義的綁定,作用域是整個(gè)程序 - 您可以在任何地方引用這種綁定。它們被稱為全局的。
但是為函數(shù)參數(shù)創(chuàng)建的,或在函數(shù)內(nèi)部聲明的綁定,只能在該函數(shù)中引用,所以它們被稱為局部綁定。 每次調(diào)用該函數(shù)時(shí),都會(huì)創(chuàng)建這些綁定的新實(shí)例。 這提供了函數(shù)之間的一些隔離 - 每個(gè)函數(shù)調(diào)用,都在它自己的小世界(它的局部環(huán)境)中運(yùn)行,并且通常可以在不知道全局環(huán)境中發(fā)生的事情的情況下理解。
用let和const聲明的綁定,實(shí)際上是它們的聲明所在的塊的局部對象,所以如果你在循環(huán)中創(chuàng)建了一個(gè),那么循環(huán)之前和之后的代碼就不能“看見”它。JavaScript 2015 之前,只有函數(shù)創(chuàng)建新的作用域,因此,使用var關(guān)鍵字創(chuàng)建的舊式綁定,在它們出現(xiàn)的整個(gè)函數(shù)中內(nèi)都可見,或者如果它們不在函數(shù)中,在全局作用域可見。
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
每個(gè)作用域都可以“向外查看”它周圍的作用域,所以示例中的塊內(nèi)可以看到x。 當(dāng)多個(gè)綁定具有相同名稱時(shí)例外 - 在這種情況下,代碼只能看到最內(nèi)層的那個(gè)。 例如,當(dāng)halve函數(shù)中的代碼引用n時(shí),它看到它自己的n,而不是全局的n。
const halve = function(n) { return n / 2; } let n = 10; console.log(halve(100)); // → 50 console.log(n); // → 10嵌套作用域
JavaScript 不僅區(qū)分全局和局部綁定。 塊和函數(shù)可以在其他塊和函數(shù)內(nèi)部創(chuàng)建,產(chǎn)生多層局部環(huán)境。
例如,這個(gè)函數(shù)(輸出制作一批鷹嘴豆泥所需的配料)的內(nèi)部有另一個(gè)函數(shù):
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函數(shù)中的代碼,可以從外部函數(shù)中看到factor綁定。 但是它的局部綁定,比如unit或ingredientAmount,在外層函數(shù)中是不可見的。
簡而言之,每個(gè)局部作用域也可以看到所有包含它的局部作用域。 塊內(nèi)可見的綁定集,由這個(gè)塊在程序文本中的位置決定。 每個(gè)局部作用域也可以看到包含它的所有局部作用域,并且所有作用域都可以看到全局作用域。 這種綁定可見性方法稱為詞法作用域。
作為值的函數(shù)函數(shù)綁定通常只充當(dāng)程序特定部分的名稱。 這樣的綁定被定義一次,永遠(yuǎn)不會(huì)改變。 這使得容易混淆函數(shù)和名稱。
let launchMissiles = function(value) { missileSystem.launch("now"); }; if (safeMode) { launchMissiles = function() {/* do nothing */}; }
在第 5 章中,我們將會(huì)討論一些高級(jí)功能:將函數(shù)類型的值傳遞給其他函數(shù)。
符號(hào)聲明創(chuàng)建函數(shù)綁定的方法稍短。 當(dāng)在語句開頭使用function關(guān)鍵字時(shí),它的工作方式不同。
function square(x) { return x * x; }
這是函數(shù)聲明。 該語句定義了綁定square并將其指向給定的函數(shù)。 寫起來稍微容易一些,并且在函數(shù)之后不需要分號(hào)。
這種形式的函數(shù)定義有一個(gè)微妙之處。
console.log("The future says:", future()); function future() { return "You"ll never have flying cars"; }
前面的代碼可以執(zhí)行,即使在函數(shù)定義在使用它的代碼下面。 函數(shù)聲明不是常規(guī)的從上到下的控制流的一部分。 在概念上,它們移到了其作用域的頂部,并可被該作用域內(nèi)的所有代碼使用。 這有時(shí)是有用的,因?yàn)樗砸环N看似有意義的方式,提供了對代碼進(jìn)行排序的自由,而無需擔(dān)心在使用之前必須定義所有函數(shù)。
箭頭函數(shù)函數(shù)的第三個(gè)符號(hào)與其他函數(shù)看起來有很大不同。 它不使用function關(guān)鍵字,而是使用由等號(hào)和大于號(hào)組成的箭頭(=>)(不要與大于等于運(yùn)算符混淆,該運(yùn)算符寫做>=)。
const power = (base, exponent) => { let result = 1; for (let count = 0; count < exponent; count++) { result *= base; } return result; };
箭頭出現(xiàn)在參數(shù)列表后面,然后是函數(shù)的主體。 它表達(dá)了一些東西,類似“這個(gè)輸入(參數(shù))產(chǎn)生這個(gè)結(jié)果(主體)”。
如果只有一個(gè)參數(shù)名稱,則可以省略參數(shù)列表周圍的括號(hào)。 如果主體是單個(gè)表達(dá)式,而不是大括號(hào)中的塊,則表達(dá)式將從函數(shù)返回。 所以這兩個(gè)square的定義是一樣的:
const square1 = (x) => { return x * x; }; const square2 = x => x * x;
當(dāng)一個(gè)箭頭函數(shù)沒有參數(shù)時(shí),它的參數(shù)列表只是一組空括號(hào)。
const horn = () => { console.log("Toot"); };
在語言中沒有很好的理由,同時(shí)擁有箭頭函數(shù)和函數(shù)表達(dá)式。 除了我們將在第 6 章中討論的一個(gè)小細(xì)節(jié)外,他們實(shí)現(xiàn)相同的東西。 在 2015 年增加了箭頭函數(shù),主要是為了能夠以簡短的方式編寫小函數(shù)表達(dá)式。 我們將在第 5 章中使用它們。
調(diào)用棧控制流經(jīng)過函數(shù)的方式有點(diǎn)復(fù)雜。 讓我們仔細(xì)看看它。 這是一個(gè)簡單的程序,它執(zhí)行了一些函數(shù)調(diào)用:
function greet(who) { console.log("Hello " + who); } greet("Harry"); console.log("Bye");
這個(gè)程序的執(zhí)行大致是這樣的:對greet的調(diào)用使控制流跳轉(zhuǎn)到該函數(shù)的開始(第 2 行)。 該函數(shù)調(diào)用控制臺(tái)的console.log來完成它的工作,然后將控制流返回到第 2 行。 它到達(dá)greet函數(shù)的末尾,所以它返回到調(diào)用它的地方,這是第 4 行。 之后的一行再次調(diào)用console.log。 之后,程序結(jié)束。
我們可以使用下圖表示出控制流:
not in function in greet in console.log in greet not in function in console.log not in function
由于函數(shù)在返回時(shí)必須跳回調(diào)用它的地方,因此計(jì)算機(jī)必須記住調(diào)用發(fā)生處上下文。 在一種情況下,console.log完成后必須返回greet函數(shù)。 在另一種情況下,它返回到程序的結(jié)尾。
計(jì)算機(jī)存儲(chǔ)此上下文的地方是調(diào)用棧。 每次調(diào)用函數(shù)時(shí),當(dāng)前上下文都存儲(chǔ)在此棧的頂部。 當(dāng)函數(shù)返回時(shí),它會(huì)從棧中刪除頂部上下文,并使用該上下文繼續(xù)執(zhí)行。
存儲(chǔ)這個(gè)棧需要計(jì)算機(jī)內(nèi)存中的空間。 當(dāng)棧變得太大時(shí),計(jì)算機(jī)將失敗,并顯示“棧空間不足”或“遞歸太多”等消息。 下面的代碼通過向計(jì)算機(jī)提出一個(gè)非常困難的問題來說明這一點(diǎn),這個(gè)問題會(huì)導(dǎo)致兩個(gè)函數(shù)之間的無限的來回調(diào)用。 相反,如果計(jì)算機(jī)有無限的棧,它將會(huì)是無限的。 事實(shí)上,我們將耗盡空間,或者“把棧頂破”。
function chicken() { return egg(); } function egg() { return chicken(); } console.log(chicken() + " came first."); // → ??可選參數(shù)
下面的代碼可以正常執(zhí)行:
function square(x) { return x * x; } console.log(square(4, true, "hedgehog")); // → 16
我們定義了square,只帶有一個(gè)參數(shù)。 然而,當(dāng)我們使用三個(gè)參數(shù)調(diào)用它時(shí),語言并不會(huì)報(bào)錯(cuò)。 它會(huì)忽略額外的參數(shù)并計(jì)算第一個(gè)參數(shù)的平方。
JavaScript 對傳入函數(shù)的參數(shù)數(shù)量幾乎不做任何限制。如果你傳遞了過多參數(shù),多余的參數(shù)就會(huì)被忽略掉,而如果你傳遞的參數(shù)過少,遺漏的參數(shù)將會(huì)被賦值成undefined。
該特性的缺點(diǎn)是你可能恰好向函數(shù)傳遞了錯(cuò)誤數(shù)量的參數(shù),但沒有人會(huì)告訴你這個(gè)錯(cuò)誤。
優(yōu)點(diǎn)是這種行為可以用于使用不同數(shù)量的參數(shù)調(diào)用一個(gè)函數(shù)。 例如,這個(gè)minus函數(shù)試圖通過作用于一個(gè)或兩個(gè)參數(shù),來模仿-運(yùn)算符:
function minus(a, b) { if (b === undefined) return -a; else return a - b; } console.log(minus(10)); // → -10 console.log(minus(10, 5)); // → 5
如果你在一個(gè)參數(shù)后面寫了一個(gè)=運(yùn)算符,然后是一個(gè)表達(dá)式,那么當(dāng)沒有提供它時(shí),該表達(dá)式的值將會(huì)替換該參數(shù)。
例如,這個(gè)版本的power使其第二個(gè)參數(shù)是可選的。 如果你沒有提供或傳遞undefined,它將默認(rèn)為 2,函數(shù)的行為就像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
在下一章當(dāng)中,我們將會(huì)了解如何獲取傳遞給函數(shù)的整個(gè)參數(shù)列表。我們可以借助于這種特性來實(shí)現(xiàn)函數(shù)接收任意數(shù)量的參數(shù)。比如console.log就利用了這種特性,它可以用來輸出所有傳遞給它的值。
console.log("C", "O", 2); // → C O 2閉包
函數(shù)可以作為值使用,而且其局部綁定會(huì)在每次函數(shù)調(diào)用時(shí)重新創(chuàng)建,由此引出一個(gè)值得我們探討的問題:如果函數(shù)已經(jīng)執(zhí)行結(jié)束,那么這些由函數(shù)創(chuàng)建的局部綁定會(huì)如何處理呢?
下面的示例代碼展示了這種情況。代碼中定義了函數(shù)wrapValue,該函數(shù)創(chuàng)建了一個(gè)局部綁定localVariable,并返回一個(gè)函數(shù),用于訪問并返回局部綁定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
這是允許的并且按照您的希望運(yùn)行 - 綁定的兩個(gè)實(shí)例仍然可以訪問。 這種情況很好地證明了一個(gè)事實(shí),每次調(diào)用都會(huì)重新創(chuàng)建局部綁定,而且不同的調(diào)用不能覆蓋彼此的局部綁定。
這種特性(可以引用封閉作用域中的局部綁定的特定實(shí)例)稱為閉包。 引用來自周圍的局部作用域的綁定的函數(shù)稱為(一個(gè))閉包。 這種行為不僅可以讓您免于擔(dān)心綁定的生命周期,而且還可以以創(chuàng)造性的方式使用函數(shù)值。
我們對上面那個(gè)例子稍加修改,就可以創(chuàng)建一個(gè)可以乘以任意數(shù)字的函數(shù)。
function multiplier(factor) { return number => number * factor; } let twice = multiplier(2); console.log(twice(5)); // → 10
由于參數(shù)本身就是一個(gè)局部綁定,所以wrapValue示例中顯式的local綁定并不是真的需要。
考慮這樣的程序需要一些實(shí)踐。 一個(gè)好的心智模型是,將函數(shù)值看作值,包含他們主體中的代碼和它們的創(chuàng)建環(huán)境。 被調(diào)用時(shí),函數(shù)體會(huì)看到它的創(chuàng)建環(huán)境,而不是它的調(diào)用環(huán)境。
這個(gè)例子調(diào)用multiplier并創(chuàng)建一個(gè)環(huán)境,其中factor參數(shù)綁定了 2。 它返回的函數(shù)值,存儲(chǔ)在twice中,會(huì)記住這個(gè)環(huán)境。 所以當(dāng)它被調(diào)用時(shí),它將它的參數(shù)乘以 2。
遞歸一個(gè)函數(shù)調(diào)用自己是完全可以的,只要它沒有經(jīng)常這樣做以致溢出棧。 調(diào)用自己的函數(shù)被稱為遞歸函數(shù)。 遞歸允許一些函數(shù)以不同的風(fēng)格編寫。 舉個(gè)例子,這是power的替代實(shí)現(xiàn):
function power(base, exponent) { if (exponent == 0) { return 1; } else { return base * power(base, exponent - 1); } } console.log(power(2, 3)); // → 8
這與數(shù)學(xué)家定義冪運(yùn)算的方式非常接近,并且可以比循環(huán)變體將該概念描述得更清楚。 該函數(shù)以更小的指數(shù)多次調(diào)用自己以實(shí)現(xiàn)重復(fù)的乘法。
但是這個(gè)實(shí)現(xiàn)有一個(gè)問題:在典型的 JavaScript 實(shí)現(xiàn)中,它大約比循環(huán)版本慢三倍。 通過簡單循環(huán)來運(yùn)行,通常比多次調(diào)用函數(shù)開銷低。
速度與優(yōu)雅的困境是一個(gè)有趣的問題。 您可以將其視為人性化和機(jī)器友好性之間的權(quán)衡。 幾乎所有的程序都可以通過更大更復(fù)雜的方式加速。 程序員必須達(dá)到適當(dāng)?shù)钠胶狻?/p>
在power函數(shù)的情況下,不雅的(循環(huán))版本仍然非常簡單易讀。 用遞歸版本替換它沒有什么意義。 然而,通常情況下,一個(gè)程序處理相當(dāng)復(fù)雜的概念,為了讓程序更直接,放棄一些效率是有幫助的。
擔(dān)心效率可能會(huì)令人分心。 這又是另一個(gè)讓程序設(shè)計(jì)變復(fù)雜的因素,當(dāng)你做了一件已經(jīng)很困難的事情時(shí),擔(dān)心的額外事情可能會(huì)癱瘓。
因此,總是先寫一些正確且容易理解的東西。 如果您擔(dān)心速度太慢 - 通常不是這樣,因?yàn)榇蠖鄶?shù)代碼的執(zhí)行不足以花費(fèi)大量時(shí)間 - 您可以事后進(jìn)行測量并在必要時(shí)進(jìn)行改進(jìn)。
遞歸并不總是循環(huán)的低效率替代方法。 遞歸比循環(huán)更容易解決解決一些問題。 這些問題通常是需要探索或處理幾個(gè)“分支”的問題,每個(gè)“分支”可能再次派生為更多的分支。
考慮這個(gè)難題:從數(shù)字 1 開始,反復(fù)加 5 或乘 3,就可以產(chǎn)生無限數(shù)量的新數(shù)字。 你會(huì)如何編寫一個(gè)函數(shù),給定一個(gè)數(shù)字,它試圖找出產(chǎn)生這個(gè)數(shù)字的,這種加法和乘法的序列?
例如,數(shù)字 13 可以通過先乘 3 然后再加 5 兩次來到達(dá),而數(shù)字 15 根本無法到達(dá)。
使用遞歸編碼的解決方案如下所示:
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)
需要注意的是該程序并不需要找出最短運(yùn)算序列,只需要找出任何一個(gè)滿足要求的序列即可。
如果你沒有看到它的工作原理,那也沒關(guān)系。 讓我們?yōu)g覽它,因?yàn)樗沁f歸思維的很好的練習(xí)。
內(nèi)層函數(shù)find進(jìn)行實(shí)際的遞歸。 它有兩個(gè)參數(shù):當(dāng)前數(shù)字和記錄我們?nèi)绾蔚竭_(dá)這個(gè)數(shù)字的字符串。 如果找到解決方案,它會(huì)返回一個(gè)字符串,顯示如何到達(dá)目標(biāo)。 如果從這個(gè)數(shù)字開始找不到解決方案,則返回null。
為此,該函數(shù)執(zhí)行三個(gè)操作之一。 如果當(dāng)前數(shù)字是目標(biāo)數(shù)字,則當(dāng)前歷史記錄是到達(dá)目標(biāo)的一種方式,因此將其返回。 如果當(dāng)前的數(shù)字大于目標(biāo),則進(jìn)一步探索該分支是沒有意義的,因?yàn)榧臃ê统朔ㄖ粫?huì)使數(shù)字變大,所以它返回null。 最后,如果我們?nèi)匀坏陀谀繕?biāo)數(shù)字,函數(shù)會(huì)嘗試從當(dāng)前數(shù)字開始的兩個(gè)可能路徑,通過調(diào)用它自己兩次,一次是加法,一次是乘法。 如果第一次調(diào)用返回非null的東西,則返回它。 否則,返回第二個(gè)調(diào)用,無論它產(chǎn)生字符串還是null。
為了更好地理解函數(shù)執(zhí)行過程,讓我們來看一下搜索數(shù)字 13 時(shí),find函數(shù)的調(diào)用情況:
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!
縮進(jìn)表示調(diào)用棧的深度。 第一次調(diào)用find時(shí),它首先調(diào)用自己來探索以(1 + 5)開始的解決方案。 這一調(diào)用將進(jìn)一步遞歸,來探索每個(gè)后續(xù)的解,它產(chǎn)生小于或等于目標(biāo)數(shù)字。 由于它沒有找到一個(gè)命中目標(biāo)的解,所以它向第一個(gè)調(diào)用返回null。 那里的||操作符會(huì)使探索(1 * 3)的調(diào)用發(fā)生。 這個(gè)搜索的運(yùn)氣更好 - 它的第一次遞歸調(diào)用,通過另一個(gè)遞歸調(diào)用,命中了目標(biāo)數(shù)字。 最內(nèi)層的調(diào)用返回一個(gè)字符串,并且中間調(diào)用中的每個(gè)“||”運(yùn)算符都會(huì)傳遞該字符串,最終返回解決方案。
添加新函數(shù)這里有兩種常用的方法,將函數(shù)引入到程序中。
首先是你發(fā)現(xiàn)自己寫了很多次非常相似的代碼。 我們最好不要這樣做。 擁有更多的代碼,意味著更多的錯(cuò)誤空間,并且想要了解程序的人閱讀更多資料。 所以我們選取重復(fù)的功能,為它找到一個(gè)好名字,并把它放到一個(gè)函數(shù)中。
第二種方法是,你發(fā)現(xiàn)你需要一些你還沒有寫的功能,這聽起來像是它應(yīng)該有自己的函數(shù)。 您將首先命名該函數(shù),然后您將編寫它的主體。 在實(shí)際定義函數(shù)本身之前,您甚至可能會(huì)開始編寫使用該函數(shù)的代碼。
給函數(shù)起名的難易程度取決于我們封裝的函數(shù)的用途是否明確。對此,我們一起來看一個(gè)例子。
我們想編寫一個(gè)打印兩個(gè)數(shù)字的程序,第一個(gè)數(shù)字是農(nóng)場中牛的數(shù)量,第二個(gè)數(shù)字是農(nóng)場中雞的數(shù)量,并在數(shù)字后面跟上Cows和Chickens用以說明,并且在兩個(gè)數(shù)字前填充 0,以使得每個(gè)數(shù)字總是由三位數(shù)字組成。
007 Cows 011 Chickens
這需要兩個(gè)參數(shù)的函數(shù) - 牛的數(shù)量和雞的數(shù)量。 讓我們來編程。
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);
在字符串表達(dá)式后面寫.length會(huì)給我們這個(gè)字符串的長度。 因此,while循環(huán)在數(shù)字字符串前面加上零,直到它們至少有三個(gè)字符的長度。
任務(wù)完成! 但就在我們即將向農(nóng)民發(fā)送代碼(連同大量發(fā)票)時(shí),她打電話告訴我們,她也開始飼養(yǎng)豬,我們是否可以擴(kuò)展軟件來打印豬的數(shù)量?
當(dāng)然沒有問題。但是當(dāng)再次復(fù)制粘貼這四行代碼的時(shí)候,我們停了下來并重新思考。一定還有更好的方案來解決我們的問題。以下是第一種嘗試:
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這個(gè)函數(shù)并不十分恰當(dāng)。它把三個(gè)操作,即打印信息、數(shù)字補(bǔ)零和添加標(biāo)簽放到了一個(gè)函數(shù)中處理。
這一次,我們不再將程序當(dāng)中重復(fù)的代碼提取成一個(gè)函數(shù),而只是提取其中一項(xiàng)操作。
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的函數(shù)具有很好的名稱,使讀取代碼的人更容易弄清它的功能。 而且這樣的函數(shù)在更多的情況下是有用的,不僅僅是這個(gè)特定程序。 例如,您可以使用它來幫助打印精確對齊的數(shù)字表格。
我們的函數(shù)應(yīng)該包括多少功能呢?我們可以編寫一個(gè)非常簡單的函數(shù),只支持將數(shù)字?jǐn)U展成 3 字符寬。也可以編寫一個(gè)復(fù)雜通用的數(shù)字格式化系統(tǒng),可以處理分?jǐn)?shù)、負(fù)數(shù)、小數(shù)點(diǎn)對齊和使用不同字符填充等。
一個(gè)實(shí)用原則是不要故作聰明,除非你確定你會(huì)需要它。 為你遇到的每一個(gè)功能編寫通用“框架”是很誘人的。 控制住那種沖動(dòng)。 你不會(huì)完成任何真正的工作 - 你只會(huì)編寫你永遠(yuǎn)不會(huì)使用的代碼。
函數(shù)及其副作用我們可以將函數(shù)分成兩類:一類調(diào)用后產(chǎn)生副作用,而另一類則產(chǎn)生返回值(當(dāng)然我們也可以定義同時(shí)產(chǎn)生副作用和返回值的函數(shù))。
在農(nóng)場案例當(dāng)中,我們調(diào)用第一個(gè)輔助函數(shù)printZeroPaddedWithLabel來產(chǎn)生副作用,打印一行文本信息。而在第二個(gè)版本中有一個(gè)zeroPad函數(shù),我們調(diào)用它來產(chǎn)生返回值。第二個(gè)函數(shù)比第一個(gè)函數(shù)的應(yīng)用場景更加廣泛,這并非偶然。相比于直接產(chǎn)生副作用的函數(shù),產(chǎn)生返回值的函數(shù)則更容易集成到新的環(huán)境當(dāng)中使用。
純函數(shù)是一種特定類型的,生成值的函數(shù),它不僅沒有副作用,而且也不依賴其他代碼的副作用,例如,它不讀取值可能會(huì)改變的全局綁定。 純函數(shù)具有令人愉快的屬性,當(dāng)用相同的參數(shù)調(diào)用它時(shí),它總是產(chǎn)生相同的值(并且不會(huì)做任何其他操作)。 這種函數(shù)的調(diào)用,可以由它的返回值代替而不改變代碼的含義。 當(dāng)你不確定純函數(shù)是否正常工作時(shí),你可以通過簡單地調(diào)用它來測試它,并且知道如果它在當(dāng)前上下文中工作,它將在任何上下文中工作。 非純函數(shù)往往需要更多的腳手架來測試。
盡管如此,我們也沒有必要覺得非純函數(shù)就不好,然后將這類函數(shù)從代碼中刪除。副作用常常是非常有用的。比如說,我們不可能去編寫一個(gè)純函數(shù)版本的console.log,但console.log依然十分實(shí)用。而在副作用的幫助下,有些操作則更易、更快實(shí)現(xiàn),因此考慮到運(yùn)算速度,有時(shí)候純函數(shù)并不可取。
本章小結(jié)本章教你如何編寫自己的函數(shù)。 當(dāng)用作表達(dá)式時(shí),function關(guān)鍵字可以創(chuàng)建一個(gè)函數(shù)值。 當(dāng)作為一個(gè)語句使用時(shí),它可以用來聲明一個(gè)綁定,并給它一個(gè)函數(shù)作為它的值。 箭頭函數(shù)是另一種創(chuàng)建函數(shù)的方式。
// 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;
理解函數(shù)的一個(gè)關(guān)鍵方面是理解作用域。 每個(gè)塊創(chuàng)建一個(gè)新的作用域。 在給定作用域內(nèi)聲明的參數(shù)和綁定是局部的,并且從外部看不到。 用var聲明的綁定行為不同 - 它們最終在最近的函數(shù)作用域或全局作用域內(nèi)。
將程序執(zhí)行的任務(wù)分成不同的功能是有幫助的。 你不必重復(fù)自己,函數(shù)可以通過將代碼分組成一些具體事物,來組織程序。
習(xí)題 最小值前一章介紹了標(biāo)準(zhǔn)函數(shù)Math.min,它可以返回參數(shù)中的最小值。我們現(xiàn)在可以構(gòu)建相似的東西。編寫一個(gè)函數(shù)min,接受兩個(gè)參數(shù),并返回其最小值。
// Your code here. console.log(min(0, 10)); // → 0 console.log(min(0, -10)); // → -10遞歸
我們已經(jīng)看到,%(取余運(yùn)算符)可以用于判斷一個(gè)數(shù)是否是偶數(shù),通過使用% 2來檢查它是否被 2 整除。這里有另一種方法來判斷一個(gè)數(shù)字是偶數(shù)還是奇數(shù):
0是偶數(shù)
1是奇數(shù)
對于其他任何數(shù)字N,其奇偶性與N–2相同。
定義對應(yīng)此描述的遞歸函數(shù)isEven。 該函數(shù)應(yīng)該接受一個(gè)參數(shù)(一個(gè)正整數(shù))并返回一個(gè)布爾值。
使用 50 與 75 測試該函數(shù)。想想如果參數(shù)為 –1 會(huì)發(fā)生什么以及產(chǎn)生相應(yīng)結(jié)果的原因。請你想一個(gè)方法來修正該問題。
// Your code here. console.log(isEven(50)); // → true console.log(isEven(75)); // → false console.log(isEven(-1)); // → ??字符計(jì)數(shù)
你可以通過編寫"string"[N],來從字符串中得到第N個(gè)字符或字母。 返回的值將是只包含一個(gè)字符的字符串(例如"b")。 第一個(gè)字符的位置為零,這會(huì)使最后一個(gè)字符在string.length - 1。 換句話說,含有兩個(gè)字符的字符串的長度為2,其字符的位置為 0 和 1。
編寫一個(gè)函數(shù)countBs,接受一個(gè)字符串參數(shù),并返回一個(gè)數(shù)字,表示該字符串中有多少個(gè)大寫字母"B"。
接著編寫一個(gè)函數(shù)countChar,和countBs作用一樣,唯一區(qū)別是接受第二個(gè)參數(shù),指定需要統(tǒng)計(jì)的字符(而不僅僅能統(tǒng)計(jì)大寫字母"B")。并使用這個(gè)新函數(shù)重寫函數(shù)countBs。
// Your code here. console.log(countBs("BBC")); // → 2 console.log(countChar("kakkerlak", "k")); // → 4
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/105026.html
摘要:來源編程精解中文第三版翻譯項(xiàng)目原文譯者飛龍協(xié)議自豪地采用谷歌翻譯部分參考了編程精解第版,這是一本關(guān)于指導(dǎo)電腦的書。在可控的范圍內(nèi)編寫程序是編程過程中首要解決的問題。我們可以用中文來描述這些指令將數(shù)字存儲(chǔ)在內(nèi)存地址中的位置。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:Introduction 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地...
摘要:相反,當(dāng)響應(yīng)指針事件時(shí),它會(huì)調(diào)用創(chuàng)建它的代碼提供的回調(diào)函數(shù),該函數(shù)將處理應(yīng)用的特定部分。回調(diào)函數(shù)可能會(huì)返回另一個(gè)回調(diào)函數(shù),以便在按下按鈕并且將指針移動(dòng)到另一個(gè)像素時(shí)得到通知。它們?yōu)榻M件構(gòu)造器的數(shù)組而提供。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:Project: A Pixel Art Editor 譯者:飛龍 協(xié)議:CC BY-NC-SA 4...
摘要:為了運(yùn)行包裹的程序,可以將這些值應(yīng)用于它們。在瀏覽器中,輸出出現(xiàn)在控制臺(tái)中。在英文版頁面上運(yùn)行示例或自己的代碼時(shí),會(huì)在示例之后顯示輸出,而不是在瀏覽器的控制臺(tái)中顯示。這被稱為條件執(zhí)行。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:Program Structure 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《J...
摘要:在本例中,使用屬性指定鏈接的目標(biāo),其中表示超文本鏈接。您應(yīng)該認(rèn)為和元數(shù)據(jù)隱式出現(xiàn)在示例中,即使它們沒有實(shí)際顯示在文本中。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:JavaScript and the Browser 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《JavaScript 編程精解(第 2 版)》 ...
摘要:來源編程精解中文第三版翻譯項(xiàng)目原文譯者飛龍協(xié)議自豪地采用谷歌翻譯部分參考了編程精解第版確定編程語言中的表達(dá)式含義的求值器只是另一個(gè)程序。若文本不是一個(gè)合法程序,解析器應(yīng)該指出錯(cuò)誤。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:Project: A Programming Language 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地采用...
閱讀 1094·2021-11-22 14:56
閱讀 1534·2019-08-30 15:55
閱讀 3376·2019-08-30 15:45
閱讀 1667·2019-08-30 13:03
閱讀 2879·2019-08-29 18:47
閱讀 3344·2019-08-29 11:09
閱讀 2652·2019-08-26 18:36
閱讀 2626·2019-08-26 13:55