跳转到内容

Underscore.js

本页使用了标题或全文手工转换
维基百科,自由的百科全书

这是本页的一个历史版本,由SunAfterRain留言 | 贡献2022年9月5日 (一) 15:31 (// Edit via Wikitext Extension for VSCode)编辑。这可能和当前版本存在着巨大的差异。

Underscore.js
開發者Jeremy Ashkenas英语Jeremy Ashkenas和Julian Gonggrijp
首次发布2009年10月28日,​15年前​(2009-10-28[1]
当前版本1.13.4(2022年6月2日,​3年前​(2022-06-02
源代码库 編輯維基數據鏈接
编程语言JavaScript
文件大小發行版 7.5 KB
開發版 68 KB
类型JavaScript函式庫
许可协议MIT
网站underscorejs.org

Underscore.js是一個綑綁常見功能的JavaScript函數庫。[2]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建立。[3]

歷史

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

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

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

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

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

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

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

內容

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

  1. 100多個泛用函數集合。文檔區分出了幾個類別:
    • 集合函數,例如findmapminmaxgroupByshuffle,這些函數可以對迭代物件的元素進行操作。
    • 數組函數,例如firstlastflattenchunkzip,這些函數可以對類數組物件進行操作。
    • 函數函數,例如bindmemoizepartialdebounce,這些函數將函數作為參數並返回具有改變屬性的新函數(高阶函数)。
    • 物件函數為最基礎的類別,包含許多也在Underscore內部使用的函數。[26]物件函數大致可以分為兩個子類:
      • 類型檢測函數,例如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。除此之外,程序員還可以使用迭代縮寫(英語:iteratee shorthand)以避開編寫迭代函數。下方示例中,迭代縮寫為字符串'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]}'

部份套用佔位符

_ also acts as a placeholder for the partial function. partial creates a partially applied英语partial application version of a function and _ can be used to leave some parameters "open" so that these can be supplied later. For example, the groupBy example from the overview can be extended as follows to turn the expression into a reusable function:

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

迭代縮寫

如同§ 功能概述和示例所述,大多數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函數,此函數會檢查鍵名與鍵值是否皆有匹配,並依照是否匹配返回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日]. 
  2. ^ Underscore.js – ein kleines Framework mit Fokus. entwickler.de. 20 June 2018 [9 July 2020] (de-DE). 
  3. ^ JavaScript Meetup City, Open (纽约时报), April 4, 2012 
  4. ^ Ashkenas, Jeremy. Underscore 0.4.0 source. cdn.rawgit.com. [1 March 2021]. 
  5. ^ Lo-Dash v2.2.1. lodash.com. [1 March 2021]. 原始内容存档于6 November 2013. 
  6. ^ Lodash Changelog - 1.0.0 rc1. github.com. 4 December 2012 [1 March 2021]. 
  7. ^ Lodash Changelog - 3.0.0. github.com. 26 January 2015 [1 March 2021]. 
  8. ^ Ashkenas, Jeremy. The Big Kahuna: Underscore + Lodash Merge Thread. github.com. 21 May 2015 [1 March 2021]. 
  9. ^ Underscore: merged pull requests with breaking changes between 21 May and 1 October 2015. github.com. [1 March 2021]. 
  10. ^ Dalton, John-David. comment on 'Core API'. github.com. 8 June 2015 [1 March 2021]. 
  11. ^ Lodash changelog 4.0.0. github.com. 12 January 2016 [1 March 2021]. 
  12. ^ @sailshq/lodash. npmjs.com. [1 March 2021]. 
  13. ^ Dalton, John-David. Merge update.. github.com. 13 February 2016 [1 March 2021]. 
  14. ^ Krebs, Adam. comment on 'Merge update.'. github.com. 17 February 2016 [1 March 2021]. 
  15. ^ 15.0 15.1 jashkenas/underscore Insights: Contributors. github.com. [1 March 2021]. 
  16. ^ lodash/lodash Insight: Contributors. github.com. [1 March 2021]. 
  17. ^ Array.prototype.map. developer.mozilla.org. [1 March 2021]. 
  18. ^ Array.prototype.filter. developer.mozilla.org. [1 March 2021]. 
  19. ^ Array.prototype.forEach. developer.mozilla.org. [1 March 2021]. 
  20. ^ _.map. underscorejs.org. [1 March 2021]. 
  21. ^ _.filter. underscorejs.org. [1 March 2021]. 
  22. ^ _.each. underscorejs.org. [1 March 2021]. 
  23. ^ Underscore.js. underscorejs.org. [1 March 2021]. 
  24. ^ JavaScript reference. developer.mozilla.org. [1 March 2021]. 
  25. ^ underscore. npmjs.com. [1 March 2021]. 
  26. ^ Gonggrijp, Julian. modules/index.js. underscorejs.org. [5 March 2021]. 
  27. ^ Gonggrijp, Julian. modules/index-default.js. underscorejs.org. [4 March 2021]. 

外部連結

參見