Underscore.js

Underscore.js是一個綑綁常見功能的JavaScript函數庫。Underscore.js提供的功能類似Prototype.js和Ruby，但其使用函數式編程而非基於原型編程。Underscore.js的文檔將自己稱為「與禮服（JQuery）和吊帶（Backbone.js）搭配的領帶（the tie to go along with jQuery's tux, and Backbone.js' suspenders.）」。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函數以使用第一個字元進行分組：

```js
import { groupBy, first } from 'underscore';

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

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

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

```js
import { groupBy } from 'underscore';

const first = array => array[0];

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

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

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

iteratee會接收三個參數：

- 集合中當前位置的值
- 該值的鍵或索引
- 整個集合

下方示例中使用了`pick`的第二個參數，過濾掉鍵名首字母不是大寫字母的屬性：

```js
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`的屬性：

```js
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`說明：

```js
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`限制了目標函數的最大呼叫頻率：

```js
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`函數，僅在尚未設置時才分配對象屬性：

```js
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樣式」，專門用於「連結」。

```js
import _, { last } from 'underscore';

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

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

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

```js
// 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`用來建立一個函數的版本，而`_`函數可用於使某些參數「打開（open）」，而這些參數可以在後續呼叫中再傳入。例如一節提到的`groupBy`範例可以改變成下方的用法以方便重複使用：

```js
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的模組系統後命名空間已無必要性。

```js
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`函數用來建立由`_`函數生成的包裝物件的修改版本。當在這種「鍊式包裝器（chained wrapper）」上調用時，每個方法都會返回一個新的包裝物件，以便使用者可以連續使用Underscore函數：

```js
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`語法：

```js
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`函數來為自己的函數啟用連結：

```js
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的迭代函數都可使用迭代縮寫（iteratee shorthand）代替取代函數。下方範例使用了前面章節的範例來演示：

```js
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`函數，此函數用於過濾鍵名與傳入值相同的鍵值。

```js
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`函數將遞迴搜尋目標屬性。

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

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

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

```js
import { countBy } from 'underscore';

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

#### 屬性雜湊

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

```js
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`。

```js
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`來增加自定義的迭代縮寫。下方範例描述如何新增正規表達法作為迭代縮寫。

```js
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']
```

## 外部連結

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

## 基本資料

- 開發者：和Julian Gonggrijp
- 發行日期：2009年10月28日
- 軟體版本週期：1.13.4
- 軟體版本週期：2022年6月2日
- 程式語言：JavaScript
- 類型：JavaScript函式庫
- 許可協議：MIT