摘要:高階函數如果一個函數操作其他函數,即將其他函數作為參數或將函數作為返回值,那么我們可以將其稱為高階函數。我們可以使用高階函數對一系列操作和值進行抽象。高階函數有多種表現形式。腳本數據集數據處理是高階函數表現突出的一個領域。
來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Higher-Order Functions
譯者:飛龍
協議:CC BY-NC-SA 4.0
自豪地采用谷歌翻譯
部分參考了《JavaScript 編程精解(第 2 版)》
Tzu-li and Tzu-ssu were boasting about the size of their latest programs. ‘Two-hundred thousand lines,’ said Tzu-li, ‘not counting comments!’ Tzu-ssu responded, ‘Pssh, mine is almost a million lines already.’ Master Yuan-Ma said, ‘My best program has five hundred lines.’ Hearing this, Tzu-li and Tzu-ssu were enlightened.
Master Yuan-Ma,《The Book of Programming》
There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies.
C.A.R. Hoare,1980 ACM Turing Award Lecture
開發大型程序通常需要耗費大量財力和物力,這絕不僅僅是因為構建程序所花費時間的問題。大型程序的復雜程度總是很高,而這些復雜性也會給開發人員帶來不少困擾,而程序錯誤或 bug 往往就是這些時候引入的。大型程序為這些 bug 提供了良好的藏身之所,因此我們更加難以在大型程序中找到它們。
讓我們簡單回顧一下前言當中的兩個示例。其中第一個程序包含了 6 行代碼并可以直接運行。
let total = 0, count = 1; while (count <= 10) { total += count; count += 1; } console.log(total);
第二個程序則依賴于外部函數才能執行,且只有一行代碼。
console.log(sum(range(1, 10)));
哪一個程序更有可能含有 bug 呢?
如果算上sum和range兩個函數的代碼量,顯然第二個程序的代碼量更大。不過,我仍然覺得第二個程序包含 bug 的可能性比第一個程序低。
之所以這么說的原因是,第二個程序編寫的代碼很好地表達了我們期望解決的問題。對于計算一組數字之和這個操作來說,我們關注的是計算范圍和求和運算,而不是循環和計數。
sum和range這兩個函數定義的操作當然會包含循環、計數和其他一些操作。但相比于將這些代碼直接寫到一起,這種表述方式更為簡單,同時也易于避免錯誤。
抽象在程序設計中,我們把這種編寫代碼的方式稱為抽象。抽象可以隱藏底層的實現細節,從更高(或更加抽象)的層次看待我們要解決的問題。
舉個例子,比較一下這兩份豌豆湯的食譜:
按照每人一杯的量將脫水豌豆放入容器中。倒水直至浸沒豌豆,然后至少將豌豆浸泡 12 個小時。將豌豆從水中取出瀝干,倒入煮鍋中,按照每人四杯水的量倒入水。將食材蓋滿整個鍋底,并慢煮 2 個小時。按照每人半個的量加入洋蔥,用刀切片,然后放入豌豆中。按照每人一根的量加入芹菜,用刀切片,然后放入豌豆當中。按照每人一根的量放入胡蘿卜,用刀切片,然后放入豌豆中。最后一起煮 10 分鐘以上即可。
第二份食譜:
一個人的量:一杯脫水豌豆、半個切好的洋蔥、一根芹菜和一根胡蘿卜。
將豌豆浸泡 12 個小時。按照每人四杯水的量倒入水,然后用文火煨 2 個小時。加入切片的蔬菜,煮 10 分鐘以上即可。
相比第一份食譜,第二份食譜更簡短且更易于理解。但你需要了解一些有關烹調的術語:浸泡、煨、切片,還有蔬菜。
在編程的時候,我們不能期望所有功能都是現成的。因此,你可能就會像第一份食譜那樣編寫你的程序,逐個編寫計算機需要執行的代碼和步驟,而忽略了這些步驟之上的抽象概念。
在編程時,注意你的抽象級別什么時候過低,是一項非常有用的技能。
重復的抽象我們已經了解的普通函數就是一種很好的構建抽象的工具。但有些時候,光有函數也不一定能夠解決我們的問題。
程序以給定次數執行某些操作很常見。 你可以為此寫一個for循環,就像這樣:
for (let i = 0; i < 10; i++) { console.log(i); }
我們是否能夠將“做某件事N次”抽象為函數? 編寫一個調用console.log N次的函數是很容易的。
function repeatLog(n) { for (let i = 0; i < n; i++) { console.log(i); } }
但如果我們想執行打印數字以外的操作該怎么辦呢?我們可以使用函數來定義我們想做的事,而函數也是值,因此我們可以將期望執行的操作封裝成函數,然后傳遞進來。
function repeat(n, action) { for (let i = 0; i < n; i++) { action(i); } } repeat(3, console.log); // → 0 // → 1 // → 2
你不必將預定義的函數傳遞給repeat。 通常情況下,你希望原地創建一個函數值。
let labels = []; repeat(5, i => { labels.push(`Unit ${i + 1}`); }); console.log(labels); // → ["Unit 1", "Unit 2", "Unit 3", "Unit 4", "Unit 5"]
這個結構有點像for循環 - 它首先描述了這種循環,然后提供了一個主體。 但是,主體現在寫為一個函數值,它被包裹在repeat調用的括號中。 這就是它必須用右小括號和右大括號閉合的原因。 在這個例子中,主體是單個小表達式,你也可以省略大括號并將循環寫成單行。
高階函數如果一個函數操作其他函數,即將其他函數作為參數或將函數作為返回值,那么我們可以將其稱為高階函數。因為我們已經看到函數就是一個普通的值,那么高階函數也就不是什么稀奇的概念了。高階這個術語來源于數學,在數學當中,函數和值的概念有著嚴格的區分。
我們可以使用高階函數對一系列操作和值進行抽象。高階函數有多種表現形式。比如你可以使用高階函數來新建另一些函數。
function greaterThan(n) { return m => m > n; } let greaterThan10 = greaterThan(10); console.log(greaterThan10(11)); // → true
你也可以使用高階函數來修改其他的函數。
function noisy(f) { return (...args) => { console.log("calling with", args); let result = f(...args); console.log("called with", args, ", returned", result); return result; }; } noisy(Math.min)(3, 2, 1); // → calling with [3, 2, 1] // → called with [3, 2, 1] , returned 1
你甚至可以使用高階函數來實現新的控制流。
function unless(test, then) { if (!test) then(); } repeat(3, n => { unless(n % 2 == 1, () => { console.log(n, "is even"); }); }); // → 0 is even // → 2 is even
有一個內置的數組方法,forEach,它提供了類似for/of循環的東西,作為一個高階函數。
["A", "B"].forEach(l => console.log(l)); // → A // → B腳本數據集
數據處理是高階函數表現突出的一個領域。 為了處理數據,我們需要一些真實數據。 本章將使用腳本書寫系統的數據集,例如拉丁文,西里爾文或阿拉伯文。
請記住第 1 章中的 Unicode,該系統為書面語言中的每個字符分配一個數字。 大多數這些字符都與特定的腳本相關聯。 該標準包含 140 個不同的腳本 - 81 個今天仍在使用,59 個是歷史性的。
雖然我只能流利地閱讀拉丁字符,但我很欣賞這樣一個事實,即人們使用其他至少 80 種書寫系統來編寫文本,其中許多我甚至不認識。 例如,以下是泰米爾語手寫體的示例。
示例數據集包含 Unicode 中定義的 140 個腳本的一些信息。 本章的編碼沙箱中提供了SCRIPTS綁定。 該綁定包含一組對象,其中每個對象都描述了一個腳本。
{ name: "Coptic", ranges: [[994, 1008], [11392, 11508], [11513, 11520]], direction: "ltr", year: -200, living: false, link: "https://en.wikipedia.org/wiki/Coptic_alphabet" }
這樣的對象會告訴你腳本的名稱,分配給它的 Unicode 范圍,書寫方向,(近似)起始時間,是否仍在使用以及更多信息的鏈接。 方向可以是從左到右的"ltr",從右到左的"rtl"(阿拉伯語和希伯來語文字的寫法),或者從上到下的"ttb"(蒙古文的寫法)。
ranges屬性包含 Unicode 字符范圍數組,每個數組都有兩元素,包含下限和上限。 這些范圍內的任何字符碼都會分配給腳本。 下限是包括的(代碼 994 是一個科普特字符),并且上限排除在外(代碼 1008 不是)。
數組過濾為了找到數據集中仍在使用的腳本,以下函數可能會有所幫助。 它過濾掉數組中未通過測試的元素:
function filter(array, test) { let passed = []; for (let element of array) { if (test(element)) { passed.push(element); } } return passed; } console.log(filter(SCRIPTS, script => script.living)); // → [{name: "Adlam", …}, …]
該函數使用名為test的參數(一個函數值)填充計算中的“間隙” - 決定要收集哪些元素的過程。
需要注意的是,filter函數并沒有從當前數組中刪除元素,而是新建了一個數組,并將滿足條件的元素存入新建的數組中。這個函數是一個“純函數”,因為該函數并未修改給定的數組。
與forEach一樣,filter函數也是標準的數組方法。本例中定義的函數只是用于展示內部實現原理。今后我們會使用以下方法來過濾數據:
console.log(SCRIPTS.filter(s => s.direction == "ttb")); // → [{name: "Mongolian", …}, …]使用map函數轉換數組
假設我們已經通過某種方式過濾了SCRIPTS數組,生成一個用于表示腳本的信息數組。但我們想創建一個包含名稱的數組,因為這樣更加易于檢查。
map方法對數組中的每個元素調用函數,然后利用返回值來構建一個新的數組,實現轉換數組的操作。新建數組的長度與輸入的數組一致,但其中的內容卻通過對每個元素調用的函數“映射”成新的形式。
function map(array, transform) { let mapped = []; for (let element of array) { mapped.push(transform(element)); } return mapped; } let rtlScripts = SCRIPTS.filter(s => s.direction == "rtl"); console.log(map(rtlScripts, s => s.name)); // → ["Adlam", "Arabic", "Imperial Aramaic", …]
與forEach和filter一樣,map也是標準的數組方法。
使用reduce匯總數據與數組有關的另一個常見事情是從它們中計算單個值。 我們的遞歸示例,匯總了一系列數字,就是這樣一個例子。 另一個例子是找到字符最多的腳本。
表示這種模式的高階操作稱為歸約(reduce)(有時也稱為折疊(fold))。 它通過反復從數組中獲取單個元素,并將其與當前值合并來構建一個值。 在對數字進行求和時,首先從數字零開始,對于每個元素,將其與總和相加。
reduce函數包含三個參數:數組、執行合并操作的函數和初始值。該函數沒有filter和map那樣直觀,所以仔細看看:
function reduce(array, combine, start) { let current = start; for (let element of array) { current = combine(current, element); } return current; } console.log(reduce([1, 2, 3, 4], (a, b) => a + b, 0)); // → 10
數組中有一個標準的reduce方法,當然和我們上面看到的那個函數一致,可以簡化合并操作。如果你的數組中包含多個元素,在調用reduce方法的時候忽略了start參數,那么該方法將會使用數組中的第一個元素作為初始值,并從第二個元素開始執行合并操作。
console.log([1, 2, 3, 4].reduce((a, b) => a + b)); // → 10
為了使用reduce(兩次)來查找字符最多的腳本,我們可以這樣寫:
function characterCount(script) { return script.ranges.reduce((count, [from, to]) => { return count + (to - from); }, 0); } console.log(SCRIPTS.reduce((a, b) => { return characterCount(a) < characterCount(b) ? b : a; })); // → {name: "Han", …}
characterCount函數通過累加范圍的大小,來減少分配給腳本的范圍。 請注意歸約器函數的參數列表中使用的解構。 `reduce"的第二次調用通過重復比較兩個腳本并返回更大的腳本,使用它來查找最大的腳本。
Unicode 標準分配了超過 89,000 個字符給漢字腳本,它成為數據集中迄今為止最大的書寫系統。 漢字是一種(有時)用于中文,日文和韓文的文字。 這些語言共享很多字符,盡管他們傾向于以不同的方式寫它們。 (基于美國的)Unicode 聯盟決定將它們看做一個多帶帶的書寫系統來保存字符碼。 這被稱為中日韓越統一表意文字(Han unification),并且仍然使一些人非常生氣。
可組合性考慮一下,我們怎樣才可以在不使用高階函數的情況下,編寫以上示例(找到最大的腳本)?代碼沒有那么糟糕。
let biggest = null; for (let script of SCRIPTS) { if (biggest == null || characterCount(biggest) < characterCount(script)) { biggest = script; } } console.log(biggest); // → {name: "Han", …}
這段代碼中多了一些綁定,雖然多了兩行代碼,但代碼邏輯還是很容易讓人理解的。
當你需要組合操作時,高階函數的價值就突顯出來了。舉個例子,我們編寫一段代碼,找出數據集中男人和女人的平均年齡。
function average(array) { return array.reduce((a, b) => a + b) / array.length; } console.log(Math.round(average( SCRIPTS.filter(s => s.living).map(s => s.year)))); // → 1185 console.log(Math.round(average( SCRIPTS.filter(s => !s.living).map(s => s.year)))); // → 209
因此,Unicode 中的死亡腳本,平均比活動腳本更老。 這不是一個非常有意義或令人驚訝的統計數據。 但是我希望你會同意,用于計算它的代碼不難閱讀。 你可以把它看作是一個流水線:我們從所有腳本開始,過濾出活動的(或死亡的)腳本,從這些腳本中抽出時間,對它們進行平均,然后對結果進行四舍五入。
你當然也可以把這個計算寫成一個大循環。
let total = 0, count = 0; for (let script of SCRIPTS) { if (script.living) { total += script.year; count += 1; } } console.log(Math.round(total / count)); // → 1185
但很難看到正在計算什么以及如何計算。 而且由于中間結果并不表示為一致的值,因此將“平均值”之類的東西提取到多帶帶的函數中,需要更多的工作。
就計算機實際在做什么而言,這兩種方法也是完全不同的。 第一個在運行filter和map的時候會建立新的數組,而第二個只會計算一些數字,從而減少工作量。 你通常可以采用可讀的方法,但是如果你正在處理巨大的數組,并且多次執行這些操作,那么抽象風格的加速就是值得的。
字符串和字符碼這個數據集的一種用途是確定一段文本所使用的腳本。 我們來看看執行它的程序。
請記住,每個腳本都有一組與其相關的字符碼范圍。 所以給定一個字符碼,我們可以使用這樣的函數來找到相應的腳本(如果有的話):
function characterScript(code) { for (let script of SCRIPTS) { if (script.ranges.some(([from, to]) => { return code >= from && code < to; })) { return script; } } return null; } console.log(characterScript(121)); // → {name: "Latin", …}
some方法是另一個高階函數。 它需要一個測試函數,并告訴你該函數是否對數組中的任何元素返回true。
但是,我們如何獲得字符串中的字符碼?
在第一章中,我提到 JavaScript 字符串被編碼為一個 16 位數字的序列。 這些被稱為代碼單元。 一個 Unicode 字符代碼最初應該能放進這樣一個單元(它給你超 65,000 個字符)。 后來人們發現它不夠用了,很多人避開了為每個字符使用更多內存的需求。 為了解決這些問題,人們發明了 UTF-16,JavaScript 字符串使用的格式 。它使用單個 16 位代碼單元描述了大多數常見字符,但是為其他字符使用一對兩個這樣的單元。
今天 UTF-16 通常被認為是一個糟糕的主意。 它似乎總是故意設計來引起錯誤。 很容易編寫程序,假裝代碼單元和字符是一個東西。 如果你的語言不使用兩個單位的字符,顯然能正常工作。 但只要有人試圖用一些不太常見的中文字符來使用這樣的程序,就會中斷。 幸運的是,隨著 emoji 符號的出現,每個人都開始使用兩個單元的字符,處理這些問題的負擔更加分散。
// Two emoji characters, horse and shoe let horseShoe = "ud83dudc34ud83dudc5f"; console.log(horseShoe.length); // → 4 console.log(horseShoe[0]); // → (Invalid half-character) console.log(horseShoe.charCodeAt(0)); // → 55357 (Code of the half-character) console.log(horseShoe.codePointAt(0)); // → 128052 (Actual code for horse emoji)
JavaScript的charCodeAt方法為你提供了一個代碼單元,而不是一個完整的字符代碼。 稍后添加的codePointAt方法確實提供了完整的 Unicode 字符。 所以我們可以使用它從字符串中獲取字符。 但傳遞給codePointAt的參數仍然是代碼單元序列的索引。 因此,要運行字符串中的所有字符,我們仍然需要處理一個字符占用一個還是兩個代碼單元的問題。
在上一章中,我提到for/of循環也可以用在字符串上。 像codePointAt一樣,這種類型的循環,是在人們敏銳地意識到 UTF-16 的問題的時候引入的。 當你用它來遍歷一個字符串時,它會給你真正的字符,而不是代碼單元。
let roseDragon = "ud83cudf45ud83dudc09"; for (let char of roseDragon) { console.log(char); // → (emoji rose) // → (emoji dragon)
如果你有一個字符(它是一個或兩個代碼單元的字符串),你可以使用codePointAt(0)來獲得它的代碼。
識別文本我們有了characterScript函數和一種正確遍歷字符的方法。 下一步將是計算屬于每個腳本的字符。 下面的計數抽象會很實用:
function countBy(items, groupName) { let counts = []; for (let item of items) { let name = groupName(item); let known = counts.findIndex(c => c.name == name); if (known == -1) { counts.push({name, count: 1}); } else { counts[known].count++; } } return counts; } console.log(countBy([1, 2, 3, 4, 5], n => n > 2)); // → [{name: false, count: 2}, {name: true, count: 3}]
countBy函數需要一個集合(我們可以用for/of來遍歷的任何東西)以及一個函數,它計算給定元素的組名。 它返回一個對象數組,每個對象命名一個組,并告訴你該組中找到的元素數量。
它使用另一個數組方法findIndex。 這個方法有點像indexOf,但它不是查找特定的值,而是查找給定函數返回true的第一個值。 像indexOf一樣,當沒有找到這樣的元素時,它返回 -1。
使用countBy,我們可以編寫一個函數,告訴我們在一段文本中使用了哪些腳本。
function textScripts(text) { let scripts = countBy(text, char => { let script = characterScript(char.codePointAt(0)); return script ? script.name : "none"; }).filter(({name}) => name != "none"); let total = scripts.reduce((n, {count}) => n + count, 0); if (total == 0) return "No scripts found"; return scripts.map(({name, count}) => { return `${Math.round(count * 100 / total)}% ${name}`; }).join(", "); } console.log(textScripts("英國的狗說"woof", 俄羅斯的狗說"тяв"")); // → 61% Han, 22% Latin, 17% Cyrillic
該函數首先按名稱對字符進行計數,使用characterScript為它們分配一個名稱,并且對于不屬于任何腳本的字符,回退到字符串"none"。 filter調用從結果數組中刪除"none"的條目,因為我們對這些字符不感興趣。
為了能夠計算百分比,我們首先需要屬于腳本的字符總數,我們可以用reduce來計算。 如果沒有找到這樣的字符,該函數將返回一個特定的字符串。 否則,它使用map將計數條目轉換為可讀的字符串,然后使用join合并它們。
本章小結能夠將函數值傳遞給其他函數,是 JavaScript 的一個非常有用的方面。 它允許我們編寫函數,用它們中的“間隙”對計算建模。 調用這些函數的代碼,可以通過提供函數值來填補間隙。
數組提供了許多有用的高階方法。 你可以使用forEach來遍歷數組中的元素。 filter方法返回一個新數組,只包含通過謂詞函數的元素。 通過將函數應用于每個元素的數組轉換,使用map來完成。 你可以使用reduce將數組中的所有元素合并為一個值。 some方法測試任何元素是否匹配給定的謂詞函數。 findIndex找到匹配謂詞的第一個元素的位置。
習題 展開聯合使用reduce方法和concat方法,將一個數組的數組“展開”成一個單個數組,包含原始數組的所有元素。
let arrays = [[1, 2, 3], [4, 5], [6]]; // Your code here. // → [1, 2, 3, 4, 5, 6]你自己的循環
編寫一個高階函數loop,提供類似for循環語句的東西。 它接受一個值,一個測試函數,一個更新函數和一個主體函數。 每次迭代中,它首先在當前循環值上運行測試函數,并在返回false時停止。 然后它調用主體函數,向其提供當前值。 最后,它調用update函數來創建一個新的值,并從頭開始。
定義函數時,可以使用常規循環來執行實際循環。
// Your code here. loop(3, n => n > 0, n => n - 1, console.log); // → 3 // → 2 // → 1every
類似于some方法,數組也有every方法。 當給定函數對數組中的每個元素返回true時,此函數返回true。 在某種程度上,some是作用于數組的||運算符的一個版本,every就像&&運算符。
將every實現為一個函數,接受一個數組和一個謂詞函數作為參數。編寫兩個版本,一個使用循環,另一個使用some方法。
function every(array, test) { // Your code here. } console.log(every([1, 3, 5], n => n < 10)); // → true console.log(every([2, 4, 16], n => n < 10)); // → false console.log(every([], n => n < 10)); // → true
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/105024.html
摘要:來源編程精解中文第三版翻譯項目原文譯者飛龍協議自豪地采用谷歌翻譯部分參考了編程精解第版,這是一本關于指導電腦的書。在可控的范圍內編寫程序是編程過程中首要解決的問題。我們可以用中文來描述這些指令將數字存儲在內存地址中的位置。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Introduction 譯者:飛龍 協議:CC BY-NC-SA 4.0 自豪地...
摘要:為了運行包裹的程序,可以將這些值應用于它們。在瀏覽器中,輸出出現在控制臺中。在英文版頁面上運行示例或自己的代碼時,會在示例之后顯示輸出,而不是在瀏覽器的控制臺中顯示。這被稱為條件執行。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Program Structure 譯者:飛龍 協議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《J...
摘要:相反,當響應指針事件時,它會調用創建它的代碼提供的回調函數,該函數將處理應用的特定部分。回調函數可能會返回另一個回調函數,以便在按下按鈕并且將指針移動到另一個像素時得到通知。它們為組件構造器的數組而提供。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Project: A Pixel Art Editor 譯者:飛龍 協議:CC BY-NC-SA 4...
摘要:來源編程精解中文第三版翻譯項目原文譯者飛龍協議自豪地采用谷歌翻譯部分參考了編程精解第版確定編程語言中的表達式含義的求值器只是另一個程序。若文本不是一個合法程序,解析器應該指出錯誤。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Project: A Programming Language 譯者:飛龍 協議:CC BY-NC-SA 4.0 自豪地采用...
摘要:在本例中,使用屬性指定鏈接的目標,其中表示超文本鏈接。您應該認為和元數據隱式出現在示例中,即使它們沒有實際顯示在文本中。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:JavaScript and the Browser 譯者:飛龍 協議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《JavaScript 編程精解(第 2 版)》 ...
閱讀 3916·2021-11-16 11:44
閱讀 3116·2021-11-12 10:36
閱讀 3373·2021-10-08 10:04
閱讀 1257·2021-09-03 10:29
閱讀 391·2019-08-30 13:50
閱讀 2605·2019-08-29 17:14
閱讀 1735·2019-08-29 15:32
閱讀 1081·2019-08-29 11:27