Underscore.js

本页使用了标题或全文手工转换
维基百科,自由的百科全书
Underscore.js
開發者Jeremy Ashkenas英语Jeremy Ashkenas和Julian Gonggrijp
首次发布2009年10月28日,​14年前​(2009-10-28[1]
当前版本
  • 1.13.6 (2022年9月23日;穩定版本)[2][3]
編輯維基數據鏈接
源代码库 編輯維基數據鏈接
编程语言JavaScript
文件大小發行版 7.5 KB
開發版 68 KB
类型JavaScript函式庫
许可协议MIT
网站underscorejs.org

Underscore.js是一個綑綁常見功能的JavaScript函數庫。[4]Underscore.js提供的功能類似Prototype.jsRuby,但其使用函数式编程而非基于原型编程。Underscore.js的文檔將自己稱為「與禮服(JQuery)和吊帶(Backbone.js)搭配的領帶(英語:the tie to go along with jQuery's tux, and Backbone.js' suspenders.)」。Underscore.js由Backbone.jsCoffeeScript的建立者Jeremy Ashkenas英语Jeremy Ashkenas建立。[5]

歷史

2009年底,為了順利開發DocumentCloud英语DocumentCloudJeremy Ashkenas英语Jeremy Ashkenas開發了Underscore。 Underscore為最早提供通用函數式編程實用程序的JavaScript函數庫之一,靈感來自Prototype.js、Oliver Steele的Functional JavaScript和John Resig的Micro-Templating。[6]

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

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

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

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

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

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

內容

簡而言之,Underscore提供了三大功能:

  1. 100多個泛用函數集合。文檔页面存档备份,存于互联网档案馆)區分出了幾個類別:
    • 集合函數,例如findmapminmaxgroupByshuffle,這些函數可以對迭代物件的元素進行操作。
    • 數組函數,例如firstlastflattenchunkzip,這些函數可以對類數組物件進行操作。
    • 函數函數,例如bindmemoizepartialdebounce,這些函數將函數作為參數並返回具有改變屬性的新函數(高阶函数)。
    • 物件函數為最基礎的類別,包含許多也在Underscore內部使用的函數。[28]物件函數大致可以分為兩個子類:
      • 類型檢測函數,例如isNumberisElementisDataView
      • 物件數據函數,例如keysextendpickomitpairsinvert,這些函數將一般物件作為數據進行操作。
    • 實用函數是一個雜項類別,其中包括瑣碎功能如identitynoop和字符串操作函數escapeunescapetemplate。此類別還包括函數iterateemixin,它們可以被視為第2點中提及的特殊工具。
  2. 特殊工具,例如chainiteratee,這些特殊工具與第1點的函數相結合用以實現更短、更清晰的語法。以庫命名的特殊函數 _ 是這些設施的核心。

需要閱讀的有文化的源代碼,以便很容易理解庫的實現方式。文檔包括源代碼的渲染版本,其中註釋在左側,邏輯在右側。註釋使用Markdown格式化,邏輯有語法高亮。從 1.11 版本開始,Underscore 是模塊化的。出於這個原因,文檔現在包括帶註釋源的模塊化版本,其中每個功能都在一個單獨的頁面上,並且import引用是可點擊的超鏈接,以及一個單一閱讀版本,其中所有功能都在一個頁面上依賴順序。

  1. 使用文学编程進行編程,故程式碼較容易閱讀,且較容易理解函數庫的實現方式。Underscore文檔加入了源始碼的渲染版本,其中註釋在左側,邏輯在右側。註釋使用Markdown格式化,邏輯加上了語法突顯。自1.11版起,Underscore進行了模組化,因此文檔也同步加入了模組化版本页面存档备份,存于互联网档案馆),每個功能都在一個單獨的頁面上,並且import引用是可點擊的超連結;同時也文檔也提供了一個匯集所有模組的版本页面存档备份,存于互联网档案馆),使用拓撲排序進行排序。

功能概述和示例

Underscore使用函数式编程,故而可以將多個函數混和成新的表達式英语expression (computer science)。例如下方的程式碼便是使用兩個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會接收三個參數:

  1. 集合中當前位置的值
  2. 該值的鍵或索引
  3. 整個集合

下方示例中使用了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']

「集合」類別中的所有函數,包括上方範例的groupBymap函數,都可以遍歷迭代對象的索引和對象的鍵。下方示例使用函數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用來建立一個函數的部份套用英语partial application版本,而_函數可用於使某些參數「打開(英語:open)」,而這些參數可以在後續呼叫中再傳入。 例如§ 功能概述和示例一節提到的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]
// }

自定義入口點

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

命名空間

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

var _ = require('underscore');

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

命名空間也可以用於區分不同模組提供的函數,比如Underscore和Async页面存档备份,存于互联网档案馆)都提供了名為each的函數,可以分別使用_.eachasync.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函數:

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內建的函數都是使用此方法來啟用連結,即先編寫為獨立函數,而後「混合」到_函數中。[29]

迭代縮寫

如同§ 功能概述和示例所述,大多數Underscore的迭代函數都可使用迭代縮寫(英語:iteratee shorthand)代替取代函數。下方範例使用了前面章節的範例來演示:

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函數,此函數會檢查鍵名與鍵值是否皆有匹配,並依照是否匹配返回truefalse

import { find } from 'underscore';

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

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

nullundefined

當傳入值是nullundefinediteratee函數返回一個恆等函數identity。此函數用於過濾數組值在JavaScript中強制轉型為布林值時為truefalse

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

參考文獻

  1. ^ Release 0.1.0 · jashkenas/underscore. GitHub. [2022年8月25日]. (原始内容存档于2022年7月8日). 
  2. ^ Release 1.13.6. 2022年9月23日 [2022年10月5日]. 
  3. ^ https://registry.npmjs.com/underscore; 检索日期: 2023年3月3日.
  4. ^ Underscore.js – ein kleines Framework mit Fokus. entwickler.de. 20 June 2018 [9 July 2020]. (原始内容存档于2021-04-14) (德语). 
  5. ^ JavaScript Meetup City, Open (纽约时报), April 4, 2012 [2022-08-19], (原始内容存档于2017-07-06) 
  6. ^ Ashkenas, Jeremy. Underscore 0.4.0 source. cdn.rawgit.com. [1 March 2021]. (原始内容存档于2021-03-23). 
  7. ^ Lo-Dash v2.2.1. lodash.com. [1 March 2021]. 原始内容存档于6 November 2013. 
  8. ^ Lodash Changelog - 1.0.0 rc1. github.com. 4 December 2012 [1 March 2021]. (原始内容存档于2022-08-17). 
  9. ^ Lodash Changelog - 3.0.0. github.com. 26 January 2015 [1 March 2021]. (原始内容存档于2022-08-17). 
  10. ^ Ashkenas, Jeremy. The Big Kahuna: Underscore + Lodash Merge Thread. github.com. 21 May 2015 [1 March 2021]. (原始内容存档于2022-08-17). 
  11. ^ Underscore: merged pull requests with breaking changes between 21 May and 1 October 2015. github.com. [1 March 2021]. (原始内容存档于2022-08-17). 
  12. ^ Dalton, John-David. comment on 'Core API'. github.com. 8 June 2015 [1 March 2021]. (原始内容存档于2022-08-17). 
  13. ^ Lodash changelog 4.0.0. github.com. 12 January 2016 [1 March 2021]. (原始内容存档于2022-08-17). 
  14. ^ @sailshq/lodash. npmjs.com. [1 March 2021]. (原始内容存档于2022-08-27). 
  15. ^ Dalton, John-David. Merge update.. github.com. 13 February 2016 [1 March 2021]. (原始内容存档于2020-10-12). 
  16. ^ Krebs, Adam. comment on 'Merge update.'. github.com. 17 February 2016 [1 March 2021]. (原始内容存档于2020-10-12). 
  17. ^ 17.0 17.1 jashkenas/underscore Insights: Contributors. github.com. [1 March 2021]. (原始内容存档于2022-08-17). 
  18. ^ lodash/lodash Insight: Contributors. github.com. [1 March 2021]. (原始内容存档于2022-08-17). 
  19. ^ Array.prototype.map. developer.mozilla.org. [1 March 2021]. (原始内容存档于2022-09-08). 
  20. ^ Array.prototype.filter. developer.mozilla.org. [1 March 2021]. (原始内容存档于2022-09-08). 
  21. ^ Array.prototype.forEach. developer.mozilla.org. [1 March 2021]. (原始内容存档于2022-09-08). 
  22. ^ _.map. underscorejs.org. [1 March 2021]. (原始内容存档于2022-09-09). 
  23. ^ _.filter. underscorejs.org. [1 March 2021]. (原始内容存档于2022-09-09). 
  24. ^ _.each. underscorejs.org. [1 March 2021]. (原始内容存档于2022-09-09). 
  25. ^ Underscore.js. underscorejs.org. [1 March 2021]. (原始内容存档于2022-09-09). 
  26. ^ JavaScript reference. developer.mozilla.org. [1 March 2021]. (原始内容存档于2022-09-04). 
  27. ^ underscore. npmjs.com. [1 March 2021]. (原始内容存档于2022-08-17). 
  28. ^ Gonggrijp, Julian. modules/index.js. underscorejs.org. [5 March 2021]. (原始内容存档于2022-08-17). 
  29. ^ Gonggrijp, Julian. modules/index-default.js. underscorejs.org. [4 March 2021]. (原始内容存档于2022-08-17). 

外部連結

參見