Underscore.js 是一個綑綁常見功能的JavaScript 函數庫。[ 2] 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 的建立者Jeremy Ashkenas 建立。[ 3]
歷史
2009年底,為了順利開發DocumentCloud ,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.assign
和Array.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提供了三大功能:
100多個泛用函數集合。文檔 (页面存档备份 ,存于互联网档案馆 )區分出了幾個類別:
集合函數 ,例如find
、map
、min
/max
、groupBy
和shuffle
,這些函數可以對迭代物件的元素進行操作。
數組函數 ,例如first
/last
、flatten
、chunk
和zip
,這些函數可以對類數組物件進行操作。
函數函數 ,例如bind
、memoize
、partial
和debounce
,這些函數將函數作為參數並返回具有改變屬性的新函數(高阶函数 )。
物件函數 為最基礎的類別,包含許多也在Underscore內部使用的函數。[ 26] 物件函數大致可以分為兩個子類:
類型檢測函數 ,例如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
用來建立一個函數的部份套用 版本,而_
函數可用於使某些參數「打開(英語: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]
// }
自定義入口點
_
也可做為自定義 Underscore函數的入口點,程序員可以根據需要調整Underscore函數的行為。具體來說,用戶可以重寫_.iteratee
以建立新的迭代縮寫 ,又或是重寫_.templateSettings
以自定義template
函數。
命名空間
_
在舊式的AMD 模組系統和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
函數用來建立由_
函數 生成的包裝物件的修改版本。當在這種「鍊式包裝器(英語: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的迭代函數都可使用迭代縮寫(英語: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
函數,此函數會檢查鍵名與鍵值是否皆有匹配,並依照是否匹配返回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']
參考文獻
外部連結
參見