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