Underscore.js

Underscore.js是一個綑綁常見功能的JavaScript函數庫。Underscore.js提供的功能類似Prototype.js和Ruby，但其使用函数式编程而非基于原型编程。Underscore.js的文檔將自己稱為「與禮服（JQuery）和吊帶（Backbone.js）搭配的領帶（）」。Underscore.js由Backbone.js和CoffeeScript的建立者建立。

 歷史 
2009年底，為了順利開發，開發了Underscore。
Underscore為最早提供通用函數式編程實用程序的JavaScript函數庫之一，靈感來自Prototype.js、Oliver Steele的Functional JavaScript和John Resig的Micro-Templating。

2012年，John-David Dalton建立Underscore的分叉Lo-Dash（現在的Lodash）。開發初期，Lo-Dash被評價為「可客製化、效能佳和附加功能多」之Underscore的替代品。儘管如此，Lodash在分叉早期階段便和Underscore的介面有不小的差異，甚至在3.0.0版本中開始更劇烈的變更，使得用戶必須要大量變更才能升級到新版本的Lodash或是從Underscore遷移到Lodash。

2015年5月，Jeremy Ashkenas透露John-David Dalton已與他取得聯繫，希望將Lodash合併回Underscore。縱然代碼風格和代碼大小可能會對合併產生困擾，Ashkenas並不反對將Lodash的一些擴充內容合併到Underscore。當時有幾個開發人員同時為Underscore和Lodash做出貢獻，這些貢獻者開始對Underscore進行更改，使其更像Lodash。

然而，在眾人為此努力的同時，Dalton對Lodash的介面進行了更大幅度的更改，並於2015年6月發布Lodash的版本4.0.0。此更改使得Lodash與Underscore介面差距更大，也凸顯了其與Lodash本身的3.x系列版本地不小差異，同時此更改也促使一些依賴Lodash的項目分叉了自己的Lodash 3發行版。

2016年2月，Dalton宣布他認為合併工作已經完成，並建議Underscore用戶切換到Lodash。然而，Underscore的維護者明確表示，Underscore依然會作為單獨的庫存在。兩個函數庫在 2016 年之後都進入了低開發活動狀態。

隨著時間的推移，較新版本的ECMAScript標準借鑒了Underscore的部分功能，例如Object.assign和Array.prototype.map。儘管這些內置函數不如Underscore等效函數強大，此變更依然使得部分人認為Underscore不再為JavaScript項目增加價值。然而，新加入的數組功能只能在數組上使用，而非如同Underscore一樣可以適用任意迭代對象。除此之外，Underscore的大部分函數仍然沒有內置對應函數。

截至2021年3月，Julian Gonggrijp正在積極開發Underscore，他於2020年3月開始做出重大貢獻。至今仍有許多函數庫依賴Underscore，npm上的每週下載次依然高達數百萬次。

 內容 

簡而言之，Underscore提供了三大功能：

 100多個泛用函數集合。文檔 區分出了幾個類別：
 集合函數，例如find、map、min／max、groupBy和shuffle，這些函數可以對迭代物件的元素進行操作。
 數組函數，例如first／last、flatten、chunk和zip，這些函數可以對類數組物件進行操作。
 函數函數，例如bind、memoize、partial和debounce，這些函數將函數作為參數並返回具有改變屬性的新函數（高阶函数）。
 物件函數為最基礎的類別，包含許多也在Underscore內部使用的函數。物件函數大致可以分為兩個子類：
 類型檢測函數，例如isNumber、isElement和isDataView。
 物件數據函數，例如keys、extend、pick／omit、pairs和invert，這些函數將一般物件作為數據進行操作。
 實用函數是一個雜項類別，其中包括瑣碎功能如identity和noop和字符串操作函數escape、unescape和template。此類別還包括函數iteratee和mixin，它們可以被視為第2點中提及的特殊工具。
 特殊工具，例如chain和iteratee，這些特殊工具與第1點的函數相結合用以實現更短、更清晰的語法。以庫命名的特殊函數 _ 是這些設施的核心。
需要閱讀的有文化的源代碼，以便很容易理解庫的實現方式。文檔包括源代碼的渲染版本，其中註釋在左側，邏輯在右側。註釋使用Markdown格式化，邏輯有語法高亮。從 1.11 版本開始，Underscore 是模塊化的。出於這個原因，文檔現在包括帶註釋源的模塊化版本，其中每個功能都在一個單獨的頁面上，並且import引用是可點擊的超鏈接，以及一個單一閱讀版本，其中所有功能都在一個頁面上依賴順序。
 使用文学编程進行編程，故程式碼較容易閱讀，且較容易理解函數庫的實現方式。Underscore文檔加入了源始碼的渲染版本，其中註釋在左側，邏輯在右側。註釋使用Markdown格式化，邏輯加上了語法突顯。自1.11版起，Underscore進行了模組化，因此文檔也同步加入了模組化版本 ，每個功能都在一個單獨的頁面上，並且import引用是可點擊的超連結；同時也文檔也提供了一個匯集所有模組的版本 ，使用拓撲排序進行排序。

 功能概述和示例 

Underscore使用函数式编程，故而可以將多個函數混和成新的。例如下方的程式碼便是使用兩個Underscore函數以使用第一個字元進行分組：

import { groupBy, first } from 'underscore';

groupBy(['avocado', 'apricot', 'cherry', 'date', 'durian'], first);

// result:
// { a: ['avocado', 'apricot'],
//   c: ['cherry'],
//   d: ['date', 'durian']
// }

除了使用Underscore內建的函數，也可以自訂函數。例如下方程式碼實踐了自己的first函數，其結果和上方程式碼相同：

import { groupBy } from 'underscore';

const first = array => array[0];

groupBy(['avocado', 'apricot', 'cherry', 'date', 'durian'], first);

Underscore內建不少這類型的函數，以便程序員可以從現有函數組合功能，而非每次都要自己實踐。

正常情況下，第一個參數會傳入迭代對象，第二個參數傳入迭代函數或iteratee。上方示例中，first是傳遞給groupBy的迭代函數。

iteratee會接收三個參數：
 集合中當前位置的值
 該值的鍵或索引
 整個集合
下方示例中使用了pick的第二個參數，過濾掉鍵名首字母不是大寫字母的屬性：

import { pick } from 'underscore';

const details = {
    Official: 'Wolfgang Amadeus Mozart',
    informal: 'Wolfie'
};
const keyIsUpper = (value, key) => key[0] === key[0].toUpperCase();

pick(details, keyIsUpper);
// {Official: 'Wolfgang Amadeus Mozart'}

許多Underscore的函數都可以當作迭代函數，如第一個範例使用的first。除此之外，程序員還可以使用迭代縮寫以避開編寫迭代函數。下方示例中，迭代縮寫為字符串'name'，以從迭代對象提取鍵值為name的屬性：

import { map } from 'underscore';

const people = [
    {name: 'Lily', age: 44, occupation: 'librarian'},
    {name: 'Harold', age: 10, occupation: 'dreamer'},
    {name: 'Sasha', age: 68, occupation: 'library developer'}
];

map(people, 'name');  // ['Lily', 'Harold', 'Sasha']

「集合」類別中的所有函數，包括上方範例的groupBy和map函數，都可以遍歷迭代對象的索引和對象的鍵。下方示例使用函數reduce說明：

import { reduce } from 'underscore';

const add = (a, b) => a + b;
const sum = numbers => reduce(numbers, add, 0);

sum([11, 12, 13]);                  // 36
sum({Alice: 9, Bob: 9, Clair: 7});  // 25

除了遍歷數組或對象的函數外，Underscore還提供了廣泛的其他常用函數，例如throttle限制了目標函數的最大呼叫頻率：

import { throttle } from 'underscore';

// The scroll event triggers very often, so the following line may
// slow down the browser.
document.body.addEventListener('scroll', expensiveUpdateFunction);

// Limit evaluation to once every 100 milliseconds.
const throttledUpdateFunction = throttle(expensiveUpdateFunction, 100);

// Much smoother user experience!
document.body.addEventListener('scroll', throttledUpdateFunction);

另一個例子是defaults函數，僅在尚未設置時才分配對象屬性：

import { defaults } from 'underscore';

const requestData = {
    url: 'wikipedia.org',
    method: 'POST',
    body: 'article text'
};
const defaultFields = {
    method: 'GET',
    headers: {'X-Requested-With': 'XMLHttpRequest'}
};

defaults(requestData, defaultFields);
// {
//     url: 'wikipedia.org',
//     method: 'POST',
//     body: 'article text',
//     headers: {'X-Requested-With': 'XMLHttpRequest'}
// }

 _函數 

Underscore的名字來源於多種用途的_函數。

 包裝函數 

Underscore的主函數_將第一個參數包裝起來，返回一個可以呼叫所有Underscore的函數的方法，此時第一個參數將作為這些函數的第一個參數傳入。此方法又被稱作「OOP樣式」，專門用於「」。

import _, { last } from 'underscore';

// "Normal" or "functional" style
last([1, 2, 3]); // 3

// "OOP style"
_([1, 2, 3]).last() // 3

可以透過.value()拿到原始傳入的值，在JavaScript的自動轉型下也會自己展開：

// Explicit unwrap
_([1, 2, 3]).value()  // [1, 2, 3]

// Automatic unwrap when coerced to number
1 + _(2)  // 3

// Automatic unwrap when coerced to string
'abc' + _('def')  // 'abcdef'

// Automatic unwrap when formatted as JSON
JSON.stringify({ a: _([1, 2]) })  // '{"a":[1,2]}'

 部份套用佔位符 

_函數還可以用來當作partial函數的佔位符。partial用來建立一個函數的版本，而_函數可用於使某些參數「打開（）」，而這些參數可以在後續呼叫中再傳入。 例如一節提到的groupBy範例可以改變成下方的用法以方便重複使用：

import _, { partial, groupBy, first } from 'underscore';

const groupByFirstChar = partial(groupBy, _, first);

groupByFirstChar(['avocado', 'apricot', 'cherry', 'date', 'durian']);
// { a: ['avocado', 'apricot'],
//   c: ['cherry'],
//   d: ['date', 'durian']
// }

groupByFirstChar(['chestnut', 'pistache', 'walnut', 'cashew']);
// { c: ['chestnut', 'cashew'],
//   p: ['pistache'],
//   w: ['walnut]
// }

 自定義入口點 

_也可做為Underscore函數的入口點，程序員可以根據需要調整Underscore函數的行為。具體來說，用戶可以重寫_.iteratee以建立新的，又或是重寫_.templateSettings以自定義template函數。

 命名空間 

_在舊式的模組系統和CommonJS模組系統中也同時作為命名空間存在，即所有Underscore函數都包含在此一命名空間中，例如_.map和_.debounce。在JavaScript發展出ES6的模組系統後命名空間已無必要性。

var _ = require('underscore');

_.groupBy(['avocado', 'apricot', 'cherry', 'date', 'durian'], _.first);

命名空間也可以用於區分不同模組提供的函數，比如Underscore和Async 都提供了名為each的函數，可以分別使用_.each和async.each來區分這些函數。

 連結 

The function chain can be used to create a modified version of the wrapper produced by _函數. When invoked on such a chained wrapper, each method returns a new wrapper so that the user can continue to process intermediate results with Underscore functions:
chain函數用來建立由_函數生成的包裝物件的修改版本。當在這種「鍊式包裝器（）」上調用時，每個方法都會返回一個新的包裝物件，以便用戶可以連續使用Underscore函數：

import { chain } from 'underscore';

const square = x => x * x;
const isOdd = x => x % 2;

chain([1, 2, 3, 4]).filter(isOdd).map(square).last()
// returns a wrapper of 9

也可以使用.value()結束Underscore函數群搭配return語法：

const add = (x, y) => x + y;

// Given an array of numbers, return the sum of the squares of
// those numbers. This could be used in a statistics library.
function sumOfSquares(numbers) {
    return chain(numbers)
        .map(square)
        .reduce(add)
        .value();
}

連結並不是Underscore內建的函數獨有的功能。程序員也可以傳遞自定義函數給mixin函數來為自己的函數啟用連結：

import { reduce, mixin } from 'underscore';

const sum = numbers => reduce(numbers, add, 0);

mixin({ sum, square });

chain([1, 2, 3]).map(square).sum().value();  // 14
chain([1, 2, 3]).sum().square().value();     // 36

實際上Underscore內建的函數都是使用此方法來啟用連結，即先編寫為獨立函數，而後「混合」到_函數中。

 迭代縮寫 

如同所述，大多數Underscore的迭代函數都可使用迭代縮寫（）代替取代函數。下方範例使用了前面章節的範例來演示：

import { map } from 'underscore';

const people = [
    {name: 'Lily', details: {age: 44, occupation: 'fire fighter'}},
    {name: 'Harold', details: {age: 10, occupation: 'dreamer'}},
    {name: 'Sasha', details: {age: 68, occupation: 'library developer'}}
];

map(people, 'name');  // ['Lily', 'Harold', 'Sasha']

實際上，這些迭代函數是將通過將簡寫值傳遞給_.iteratee來確定實際調用的函數，而_.iteratee默認為Underscore內建的iteratee函數，此函數根據參數值返回下列幾種函數：

 路徑 

當傳入值是一個字符串，iteratee函數會將傳入值傳入property函數，此函數用於過濾鍵名與傳入值相同的鍵值。

import { iteratee, property } from 'underscore';

map(people, 'name');
map(people, iteratee('name'));
map(people, property('name'));
map(people, obj => obj && obj['name']);
// ['Lily', 'Harold', 'Sasha']

也可以傳入陣列。當傳入陣列時，property函數將遞迴搜尋目標屬性。

map(people, ['details', 'occupation']);
// ['fire fighter', 'dreamer', 'library developer']

也可以傳入數字，傳入數字時將作為數組和字符串索引。

結合上方功能，下方範例給出了計算職業名稱中第二個字符出現的次數的方法：

import { countBy } from 'underscore';

countBy(people, ['details', 'occupation', 1]);  // {i: 2, r: 1}

 屬性雜湊 

當傳入值是一個物件，iteratee函數會將傳入值傳入matcher函數，此函數會檢查鍵名與鍵值是否皆有匹配，並依照是否匹配返回true或false。

import { find } from 'underscore';

find(people, {name: 'Sasha'});
// {name: 'Sasha', details: {age: 68, occupation: 'library developer'}}

find(people, {name: 'Walter'});
// undefined

 null和undefined 

當傳入值是null或undefined，iteratee函數返回一個恆等函數identity。此函數用於過濾數組值在JavaScript中強制轉型為布林值時為true或false。

import { filter, iteratee, identity } from 'underscore';

const example = [0, 1, '', 'abc', true, false, {}];

// The following expressions are all equivalent.
filter(example);
filter(example, undefined);
filter(example, iteratee(undefined));
filter(example, identity);
// [1, 'abc', true, {}]

 覆蓋_.iteratee 

程序員可以通過覆蓋_.iteratee來增加自定義的迭代縮寫。下方範例描述如何新增正規表達法作為迭代縮寫。

import {
    iteratee as originalIteratee,
    isRegExp,
    mixin,
    filter,
} from 'underscore';

function iteratee(value, context) {
    if (isRegExp(value)) {
        return string => value.test(string);
    } else {
        return originalIteratee(value, context);
    }
}

mixin({iteratee});

filter(['absolutely', 'amazing', 'fabulous', 'trousers'], /ab/);
// ['absolutely', 'fabulous']

 參考文獻 

 外部連結 

Functional Javascript by Oliver Steele (internet archive; osteele.com only retains a screenshot)
JavaScript Micro-Templating  by John Resig, the original inspiration for _.template 

 參見 

 Lodash
 Prototype.js
 Web应用框架
 JavaScript函式庫

Category:JavaScript函式庫
Category:使用MIT许可证的软件