<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                合規國際互聯網加速 OSASE為企業客戶提供高速穩定SD-WAN國際加速解決方案。 廣告
                ## 五、高階函數 > 原文:[Higher-Order Functions](http://eloquentjavascript.net/05_higher_order.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/) > 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 ![](https://img.kancloud.cn/af/55/af55614cbfd2273c0fe4ec0279992c6f_420x247.jpg) 開發大型程序通常需要耗費大量財力和物力,這絕不僅僅是因為構建程序所花費時間的問題。大型程序的復雜程度總是很高,而這些復雜性也會給開發人員帶來不少困擾,而程序錯誤或 bug 往往就是這些時候引入的。大型程序為這些 bug 提供了良好的藏身之所,因此我們更加難以在大型程序中找到它們。 讓我們簡單回顧一下前言當中的兩個示例。其中第一個程序包含了 6 行代碼并可以直接運行。 ```js let total = 0, count = 1; while (count <= 10) { total += count; count += 1; } console.log(total); ``` 第二個程序則依賴于外部函數才能執行,且只有一行代碼。 ```js console.log(sum(range(1, 10))); ``` 哪一個程序更有可能含有 bug 呢? 如果算上`sum`和`range`兩個函數的代碼量,顯然第二個程序的代碼量更大。不過,我仍然覺得第二個程序包含 bug 的可能性比第一個程序低。 之所以這么說的原因是,第二個程序編寫的代碼很好地表達了我們期望解決的問題。對于計算一組數字之和這個操作來說,我們關注的是計算范圍和求和運算,而不是循環和計數。 `sum`和`range`這兩個函數定義的操作當然會包含循環、計數和其他一些操作。但相比于將這些代碼直接寫到一起,這種表述方式更為簡單,同時也易于避免錯誤。 ## 抽象 在程序設計中,我們把這種編寫代碼的方式稱為抽象。抽象可以隱藏底層的實現細節,從更高(或更加抽象)的層次看待我們要解決的問題。 舉個例子,比較一下這兩份豌豆湯的食譜: 按照每人一杯的量將脫水豌豆放入容器中。倒水直至浸沒豌豆,然后至少將豌豆浸泡 12 個小時。將豌豆從水中取出瀝干,倒入煮鍋中,按照每人四杯水的量倒入水。將食材蓋滿整個鍋底,并慢煮 2 個小時。按照每人半個的量加入洋蔥,用刀切片,然后放入豌豆中。按照每人一根的量加入芹菜,用刀切片,然后放入豌豆當中。按照每人一根的量放入胡蘿卜,用刀切片,然后放入豌豆中。最后一起煮 10 分鐘以上即可。 第二份食譜: 一個人的量:一杯脫水豌豆、半個切好的洋蔥、一根芹菜和一根胡蘿卜。 將豌豆浸泡 12 個小時。按照每人四杯水的量倒入水,然后用文火煨 2 個小時。加入切片的蔬菜,煮 10 分鐘以上即可。 相比第一份食譜,第二份食譜更簡短且更易于理解。但你需要了解一些有關烹調的術語:浸泡、煨、切片,還有蔬菜。 在編程的時候,我們不能期望所有功能都是現成的。因此,你可能就會像第一份食譜那樣編寫你的程序,逐個編寫計算機需要執行的代碼和步驟,而忽略了這些步驟之上的抽象概念。 在編程時,注意你的抽象級別什么時候過低,是一項非常有用的技能。 ## 重復的抽象 我們已經了解的普通函數就是一種很好的構建抽象的工具。但有些時候,光有函數也不一定能夠解決我們的問題。 程序以給定次數執行某些操作很常見。 你可以為此寫一個`for`循環,就像這樣: ```js for (let i = 0; i < 10; i++) { console.log(i); } ``` 我們是否能夠將“做某件事`N`次”抽象為函數? 編寫一個調用`console.log` `N`次的函數是很容易的。 ```js function repeatLog(n) { for (let i = 0; i < n; i++) { console.log(i); } } ``` 但如果我們想執行打印數字以外的操作該怎么辦呢?我們可以使用函數來定義我們想做的事,而函數也是值,因此我們可以將期望執行的操作封裝成函數,然后傳遞進來。 ```js function repeat(n, action) { for (let i = 0; i < n; i++) { action(i); } } repeat(3, console.log); // → 0 // → 1 // → 2 ``` 你不必將預定義的函數傳遞給`repeat`。 通常情況下,你希望原地創建一個函數值。 ```js 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`調用的括號中。 這就是它必須用右小括號和右大括號閉合的原因。 在這個例子中,主體是單個小表達式,你也可以省略大括號并將循環寫成單行。 ## 高階函數 如果一個函數操作其他函數,即將其他函數作為參數或將函數作為返回值,那么我們可以將其稱為高階函數。因為我們已經看到函數就是一個普通的值,那么高階函數也就不是什么稀奇的概念了。高階這個術語來源于數學,在數學當中,函數和值的概念有著嚴格的區分。 我們可以使用高階函數對一系列操作和值進行抽象。高階函數有多種表現形式。比如你可以使用高階函數來新建另一些函數。 ```js function greaterThan(n) { return m => m > n; } let greaterThan10 = greaterThan(10); console.log(greaterThan10(11)); // → true ``` 你也可以使用高階函數來修改其他的函數。 ```js 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 ``` 你甚至可以使用高階函數來實現新的控制流。 ```js 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`循環的東西,作為一個高階函數。 ```js ["A", "B"].forEach(l => console.log(l)); // → A // → B ``` ## 腳本數據集 數據處理是高階函數表現突出的一個領域。 為了處理數據,我們需要一些真實數據。 本章將使用腳本書寫系統的數據集,例如拉丁文,西里爾文或阿拉伯文。 請記住第 1 章中的 Unicode,該系統為書面語言中的每個字符分配一個數字。 大多數這些字符都與特定的腳本相關聯。 該標準包含 140 個不同的腳本 - 81 個今天仍在使用,59 個是歷史性的。 雖然我只能流利地閱讀拉丁字符,但我很欣賞這樣一個事實,即人們使用其他至少 80 種書寫系統來編寫文本,其中許多我甚至不認識。 例如,以下是泰米爾語手寫體的示例。 ![](https://img.kancloud.cn/93/7f/937f10fa13dac2510b4ce75b55ceab0d_559x105.png) 示例數據集包含 Unicode 中定義的 140 個腳本的一些信息。 本章的[編碼沙箱](https://eloquentjavascript.net/code#5)中提供了`SCRIPTS`綁定。 該綁定包含一組對象,其中每個對象都描述了一個腳本。 ```json { 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 不是)。 ## 數組過濾 為了找到數據集中仍在使用的腳本,以下函數可能會有所幫助。 它過濾掉數組中未通過測試的元素: ```js 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`函數也是標準的數組方法。本例中定義的函數只是用于展示內部實現原理。今后我們會使用以下方法來過濾數據: ```js console.log(SCRIPTS.filter(s => s.direction == "ttb")); // → [{name: "Mongolian", …}, …] ``` ## 使用`map`函數轉換數組 假設我們已經通過某種方式過濾了`SCRIPTS`數組,生成一個用于表示腳本的信息數組。但我們想創建一個包含名稱的數組,因為這樣更加易于檢查。 `map`方法對數組中的每個元素調用函數,然后利用返回值來構建一個新的數組,實現轉換數組的操作。新建數組的長度與輸入的數組一致,但其中的內容卻通過對每個元素調用的函數“映射”成新的形式。 ```js 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`那樣直觀,所以仔細看看: ```js 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`參數,那么該方法將會使用數組中的第一個元素作為初始值,并從第二個元素開始執行合并操作。 ```js console.log([1, 2, 3, 4].reduce((a, b) => a + b)); // → 10 ``` 為了使用`reduce`(兩次)來查找字符最多的腳本,我們可以這樣寫: ```js 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),并且仍然使一些人非常生氣。 ## 可組合性 考慮一下,我們怎樣才可以在不使用高階函數的情況下,編寫以上示例(找到最大的腳本)?代碼沒有那么糟糕。 ```js let biggest = null; for (let script of SCRIPTS) { if (biggest == null || characterCount(biggest) < characterCount(script)) { biggest = script; } } console.log(biggest); // → {name: "Han", …} ``` 這段代碼中多了一些綁定,雖然多了兩行代碼,但代碼邏輯還是很容易讓人理解的。 當你需要組合操作時,高階函數的價值就突顯出來了。舉個例子,我們編寫一段代碼,找出數據集中男人和女人的平均年齡。 ```js 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 中的死亡腳本,平均比活動腳本更老。 這不是一個非常有意義或令人驚訝的統計數據。 但是我希望你會同意,用于計算它的代碼不難閱讀。 你可以把它看作是一個流水線:我們從所有腳本開始,過濾出活動的(或死亡的)腳本,從這些腳本中抽出時間,對它們進行平均,然后對結果進行四舍五入。 你當然也可以把這個計算寫成一個大循環。 ```js 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`的時候會建立新的數組,而第二個只會計算一些數字,從而減少工作量。 你通常可以采用可讀的方法,但是如果你正在處理巨大的數組,并且多次執行這些操作,那么抽象風格的加速就是值得的。 ## 字符串和字符碼 這個數據集的一種用途是確定一段文本所使用的腳本。 我們來看看執行它的程序。 請記住,每個腳本都有一組與其相關的字符碼范圍。 所以給定一個字符碼,我們可以使用這樣的函數來找到相應的腳本(如果有的話): ```js 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 符號的出現,每個人都開始使用兩個單元的字符,處理這些問題的負擔更加分散。 ```js // Two emoji characters, horse and shoe let horseShoe = "\ud83d\udc34\ud83d\udc5f"; 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 的問題的時候引入的。 當你用它來遍歷一個字符串時,它會給你真正的字符,而不是代碼單元。 ```js let roseDragon = "\ud83c\udf45\ud83d\udc09"; for (let char of roseDragon) { console.log(char); // → (emoji rose) // → (emoji dragon) ``` 如果你有一個字符(它是一個或兩個代碼單元的字符串),你可以使用`codePointAt(0)`來獲得它的代碼。 ## 識別文本 我們有了`characterScript`函數和一種正確遍歷字符的方法。 下一步將是計算屬于每個腳本的字符。 下面的計數抽象會很實用: ```js 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`,我們可以編寫一個函數,告訴我們在一段文本中使用了哪些腳本。 ```js 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`方法,將一個數組的數組“展開”成一個單個數組,包含原始數組的所有元素。 ```js let arrays = [[1, 2, 3], [4, 5], [6]]; // Your code here. // → [1, 2, 3, 4, 5, 6] ``` ### 你自己的循環 編寫一個高階函數`loop`,提供類似`for`循環語句的東西。 它接受一個值,一個測試函數,一個更新函數和一個主體函數。 每次迭代中,它首先在當前循環值上運行測試函數,并在返回`false`時停止。 然后它調用主體函數,向其提供當前值。 最后,它調用`update`函數來創建一個新的值,并從頭開始。 定義函數時,可以使用常規循環來執行實際循環。 ```js // Your code here. loop(3, n => n > 0, n => n - 1, console.log); // → 3 // → 2 // → 1 ``` ### `every` 類似于`some`方法,數組也有`every`方法。 當給定函數對數組中的每個元素返回`true`時,此函數返回`true`。 在某種程度上,`some`是作用于數組的`||`運算符的一個版本,`every`就像`&&`運算符。 將`every`實現為一個函數,接受一個數組和一個謂詞函數作為參數。編寫兩個版本,一個使用循環,另一個使用`some`方法。 ```js 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 ```
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看