JavaScript是函數(shù)式編程語(yǔ)言,支持高階函數(shù)和閉包。你會(huì)發(fā)現(xiàn)Array有map()和filter()方法,而Object沒(méi)有這些方法,那該如何解決呢?
- 自己把這些方法添加到Array.prototype中,然后給Object.prototype也加上mapObject()等類(lèi)似的方法。
- 直接找一個(gè)成熟可靠的第三方開(kāi)源庫(kù),使用統(tǒng)一的函數(shù)來(lái)實(shí)現(xiàn)map()、filter()這些操作,比如underscore。
正如jQuery統(tǒng)一了不同瀏覽器之間DOM操作的差異,讓我們可以簡(jiǎn)單地對(duì)DOM進(jìn)行操作,underscore則提供了一套完善的函數(shù)式編程的接口,讓我們更方便地在JavaScript中實(shí)現(xiàn)函數(shù)式編程。jQuery在加載時(shí)會(huì)把自身綁定到唯一的全局變量$上,underscore與其類(lèi)似也會(huì)把自身綁定到唯一的全局變量_上,這也是為啥它叫underscore的原因。
用underscore實(shí)現(xiàn)map()操作如下:
// 數(shù)組
_.map([1, 2, 3], (x) => x * x); // [1, 4, 9]
// 對(duì)象
_.map({ a: 1, b: 2, c: 3 }, (v, k) => k + '=' + v); // ['a=1', 'b=2', 'c=3']
Collection
underscore為集合類(lèi)對(duì)象(Array和Object,不支持Map和Set)提供了一致的接口。
map/filter
與Array的map()與filter()類(lèi)似,此外underscore的map()和filter()可以作用于Object,通過(guò)調(diào)用函數(shù)function (value, key),第一個(gè)參數(shù)接收value,第二個(gè)參數(shù)接收key:
var obj = {
name: 'bob',
school: 'No.1 middle school',
address: 'xueyuan road'
};
var upper = _.map(obj, function (value, key) {
return ???;
});
alert(JSON.stringify(upper));
對(duì)Object進(jìn)行map()操作返回的是Array,如果想要返回Object必須用_.mapObject。
every / some
當(dāng)集合的所有元素都滿足條件時(shí),.every()函數(shù)返回true,當(dāng)集合中至少有一個(gè)元素滿足條件時(shí),.some()函數(shù)返回true:
// 所有元素都大于0?
_.every([1, 4, 7, -3, -9], (x) => x > 0); // false
// 至少一個(gè)元素大于0?
_.some([1, 4, 7, -3, -9], (x) => x > 0); // true
// 當(dāng)集合是Object時(shí),我們可以同時(shí)獲得value和key:
var obj = {
name: 'bob',
school: 'No.1 middle school',
address: 'xueyuan road'
};
// 判斷key和value是否全部是小寫(xiě):
var r1 = _.every(obj, function (value, key) {
return ???;
});
var r2 = _.some(obj, function (value, key) {
return ???;
});
alert('every key-value are lowercase: ' + r1 + '\nsome key-value are lowercase: ' + r2);
max / min
這兩個(gè)函數(shù)直接返回集合中最大和最小的數(shù):
var arr = [3, 5, 7, 9];
_.max(arr); // 9
_.min(arr); // 3
// 空集合會(huì)返回-Infinity和Infinity,所以要先判斷集合不為空:
_.max([])
-Infinity
_.min([])
Infinity
// 如果集合是Object,max()和min()只作用于value,忽略掉key:
_.max({ a: 1, b: 2, c: 3 }); // 3
groupBy
groupBy()把集合的元素按照key歸類(lèi),key由傳入的函數(shù)返回:
var scores = [20, 81, 75, 40, 91, 59, 77, 66, 72, 88, 99];
var groups = _.groupBy(scores, function (x) {
if (x < 60) {
return 'C';
} else if (x < 80) {
return 'B';
} else {
return 'A';
}
});
// 結(jié)果:
// {
// A: [81, 91, 88, 99],
// B: [75, 77, 66, 72],
// C: [20, 40, 59]
// }
shuffle / sample
shuffle()用洗牌算法隨機(jī)打亂一個(gè)集合:
// 注意每次結(jié)果都不一樣:
_.shuffle([1, 2, 3, 4, 5, 6]); // [3, 5, 4, 6, 2, 1]
sample()則是隨機(jī)選擇一個(gè)或多個(gè)元素,但每次結(jié)果都不一樣:
// 隨機(jī)選1個(gè):
_.sample([1, 2, 3, 4, 5, 6]); // 2
// 隨機(jī)選3個(gè):
_.sample([1, 2, 3, 4, 5, 6], 3); // [6, 1, 4]
Array
underscore為Array提供了許多工具類(lèi)方法,可以更方便快捷地操作Array。
first / last
顧名思義,這兩個(gè)函數(shù)分別取第一個(gè)和最后一個(gè)元素。
flatten
flatten()接收一個(gè)Array,無(wú)論這個(gè)Array里面嵌套了多少個(gè)Array,最后都會(huì)成為一個(gè)一維數(shù)組:
_.flatten([1, [2], [3, [[4], [5]]]]); // [1, 2, 3, 4, 5]
zip / unzip
zip()把兩個(gè)或多個(gè)數(shù)組的所有元素按索引對(duì)齊,然后按索引合并成新數(shù)組。例如,你有一個(gè)Array保存了名字,另一個(gè)Array保存了分?jǐn)?shù),現(xiàn)在,要把名字和分?jǐn)?shù)給對(duì)上,用zip()輕松實(shí)現(xiàn):
var names = ['Adam', 'Lisa', 'Bart'];
var scores = [85, 92, 59];
_.zip(names, scores);
// [['Adam', 85], ['Lisa', 92], ['Bart', 59]]
unzip()則是反過(guò)來(lái):
var namesAndScores = [['Adam', 85], ['Lisa', 92], ['Bart', 59]];
_.unzip(namesAndScores);
// [['Adam', 'Lisa', 'Bart'], [85, 92, 59]]
object
如果要想zip()那樣處理返回一個(gè)Object就要用object()函數(shù)了:
var names = ['Adam', 'Lisa', 'Bart'];
var scores = [85, 92, 59];
_.object(names, scores);
// {Adam: 85, Lisa: 92, Bart: 59}
range
range()讓你快速生成一個(gè)序列,不再需要用for循環(huán)實(shí)現(xiàn)了:
// 從0開(kāi)始小于10:
_.range(10); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
// 從1開(kāi)始小于11:
_.range(1, 11); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// 從0開(kāi)始小于30,步長(zhǎng)5:
_.range(0, 30, 5); // [0, 5, 10, 15, 20, 25]
// 從0開(kāi)始大于-10,步長(zhǎng)-1:
_.range(0, -10, -1); // [0, -1, -2, -3, -4, -5, -6, -7, -8, -9]
Function
為了充分發(fā)揮JavaScript的函數(shù)式編程特性,underscore也提供了大量JavaScript本身沒(méi)有的高階函數(shù)。
bind
bind()可以把系統(tǒng)方法與我們自己定義的方法綁定在一起,舉例如下:
// 輸出Hello, world!
var log = console.log;
// 調(diào)用call并傳入console對(duì)象作為this:
log.call(console, 'Hello, world!')
// 使用bind()
var log = _.bind(console.log, console);
log('Hello, world!');
// 錯(cuò)誤寫(xiě)法
console.log('Hello, world!');
var log = console.log;
log('Hello, world!');
partial
partial()就是為一個(gè)函數(shù)創(chuàng)建偏函數(shù),偏函數(shù)的作用可以通過(guò)例子來(lái)說(shuō)明:
假設(shè)我們要計(jì)算xy,這時(shí)只需要調(diào)用Math.pow(x, y)就可以了。但如果要經(jīng)常計(jì)算2y,每次都寫(xiě)Math.pow(2, y)就比較麻煩,如果創(chuàng)建一個(gè)新的函數(shù)能直接這樣寫(xiě)pow2N(y)就好了,這個(gè)新函數(shù)pow2N(y)就是根據(jù)Math.pow(x, y)創(chuàng)建出來(lái)的偏函數(shù),它固定住了原函數(shù)的第一個(gè)參數(shù)(始終為2):
// 固定第一個(gè)參數(shù)
var pow2N = _.partial(Math.pow, 2);
pow2N(3); // 8
pow2N(5); // 32
pow2N(10); // 1024
// 固定第二個(gè)參數(shù)
var cube = _.partial(Math.pow, _, 3);
cube(3); // 27
cube(5); // 125
cube(10); // 1000
memoize
如果調(diào)用一個(gè)函數(shù)耗時(shí)較長(zhǎng),我們可能就希望能把結(jié)果緩存下來(lái),以便后續(xù)使用。比如說(shuō)計(jì)算階乘就比較耗時(shí):
function factorial(n) {
console.log('start calculate ' + n + '!...');
var s = 1, i = n;
while (i > 1) {
s = s * i;
i --;
}
console.log(n + '! = ' + s);
return s;
}
factorial(10); // 3628800
// 注意控制臺(tái)輸出:
// start calculate 10!...
// 10! = 3628800
// 用memoize()就可以自動(dòng)緩存函數(shù)計(jì)算的結(jié)果:
var factorial = _.memoize(function(n) {
console.log('start calculate ' + n + '!...');
var s = 1, i = n;
while (i > 1) {
s = s * i;
i --;
}
console.log(n + '! = ' + s);
return s;
});
// 第一次調(diào)用:
factorial(10); // 3628800
// 注意控制臺(tái)輸出:
// start calculate 10!...
// 10! = 3628800
// 第二次調(diào)用:
factorial(10); // 3628800
// 控制臺(tái)沒(méi)有輸出
對(duì)于相同的調(diào)用,比如連續(xù)兩次調(diào)用factorial(10),第二次調(diào)用并沒(méi)有計(jì)算,而是直接返回上次計(jì)算后緩存的結(jié)果。不過(guò),當(dāng)你計(jì)算factorial(9)的時(shí)候,仍然會(huì)重新計(jì)算。
// 對(duì)factorial()進(jìn)行遞歸調(diào)用:
var factorial = _.memoize(function(n) {
console.log('start calculate ' + n + '!...');
if (n < 2) {
return 1;
}
return n * factorial(n - 1);
});
factorial(10); // 3628800
// 輸出結(jié)果說(shuō)明factorial(1)~factorial(10)都已經(jīng)緩存了:
// start calculate 10!...
// start calculate 9!...
// start calculate 8!...
// start calculate 7!...
// start calculate 6!...
// start calculate 5!...
// start calculate 4!...
// start calculate 3!...
// start calculate 2!...
// start calculate 1!...
factorial(9); // 362880
// console無(wú)輸出
once
顧名思義,once()保證某個(gè)函數(shù)執(zhí)行且僅執(zhí)行一次。如果你有一個(gè)方法叫register(),用戶在頁(yè)面上點(diǎn)兩個(gè)按鈕的任何一個(gè)都可以執(zhí)行的話,就可以用once()保證函數(shù)僅調(diào)用一次,無(wú)論用戶點(diǎn)擊多少次:
var register = _.once(function () {
alert('Register ok!');
});
// 測(cè)試效果:
register();
register();
register();
delay
delay()可以讓一個(gè)函數(shù)延遲執(zhí)行,效果和setTimeout()是一樣的,但是代碼明顯簡(jiǎn)單了:
// 2秒后調(diào)用alert():
_.delay(alert, 2000);
// 如果要延遲調(diào)用的函數(shù)有參數(shù),把參數(shù)也傳進(jìn)去
var log = _.bind(console.log, console);
_.delay(log, 2000, 'Hello,', 'world!');
// 2秒后打印'Hello, world!':
Object
和Array類(lèi)似,underscore也提供了大量針對(duì)Object的函數(shù)。
keys / allKeys
keys()可以非常方便地返回一個(gè)object自身所有的key,但不包含從原型鏈繼承下來(lái)的:
function Student(name, age) {
this.name = name;
this.age = age;
}
var xiaoming = new Student('小明', 20);
_.keys(xiaoming); // ['name', 'age']
allKeys()除了object自身的key,還包含從原型鏈繼承下來(lái)的:
function Student(name, age) {
this.name = name;
this.age = age;
}
Student.prototype.school = 'No.1 Middle School';
var xiaoming = new Student('小明', 20);
_.allKeys(xiaoming); // ['name', 'age', 'school']
values
和keys()類(lèi)似,values()返回object自身但不包含原型鏈繼承的所有值,但沒(méi)有allValues():
var obj = {
name: '小明',
age: 20
};
_.values(obj); // ['小明', 20]
mapObject
mapObject()就是針對(duì)object的map()版本:
var obj = { a: 1, b: 2, c: 3 };
// 注意傳入的函數(shù)簽名,value在前,key在后:
_.mapObject(obj, (v, k) => 100 + v); // { a: 101, b: 102, c: 103 }
invert
invert()把object的每個(gè)key-value來(lái)個(gè)交換,key變成value,value變成key:
var obj = {
Adam: 90,
Lisa: 85,
Bart: 59
};
_.invert(obj); // { '59': 'Bart', '85': 'Lisa', '90': 'Adam' }
extend / extendOwn
extend()把多個(gè)object的key-value合并到第一個(gè)object并返回:
var a = {name: 'Bob', age: 20};
_.extend(a, {age: 15}, {age: 88, city: 'Beijing'}); // {name: 'Bob', age: 88, city: 'Beijing'}
// 變量a的內(nèi)容也改變了:
a; // {name: 'Bob', age: 88, city: 'Beijing'}
注意:如果有相同的key,后面的object的value將覆蓋前面的object的value。extendOwn()和extend()類(lèi)似,但獲取屬性時(shí)忽略從原型鏈繼承下來(lái)的屬性。
clone
如果我們要復(fù)制一個(gè)object對(duì)象,就可以用clone()方法,它會(huì)把原有對(duì)象的所有屬性都復(fù)制到新的對(duì)象中:
var source = {
name: '小明',
age: 20,
skills: ['JavaScript', 'CSS', 'HTML']
};
var copied = _.clone(source);
alert(JSON.stringify(copied, null, ' '));
注意,clone()是“淺復(fù)制”,兩個(gè)對(duì)象相同的key所引用的value其實(shí)是同一對(duì)象:
source.skills === copied.skills; // true
也就是說(shuō)修改source.skills會(huì)影響copied.skills。
isEqual
isEqual()對(duì)兩個(gè)object進(jìn)行深度比較,如果內(nèi)容完全相同,則返回true:
var o1 = { name: 'Bob', skills: { Java: 90, JavaScript: 99 }};
var o2 = { name: 'Bob', skills: { JavaScript: 99, Java: 90 }};
o1 === o2; // false
_.isEqual(o1, o2); // true
isEqual()其實(shí)對(duì)Array也可以比較:
var o1 = ['Bob', { skills: ['Java', 'JavaScript'] }];
var o2 = ['Bob', { skills: ['Java', 'JavaScript'] }];
o1 === o2; // false
_.isEqual(o1, o2); // true
Chaining
underscore提供了把對(duì)象包裝成能進(jìn)行鏈?zhǔn)秸{(diào)用的方法,就是chain()函數(shù):
_.chain([1, 4, 9, 16, 25])
.map(Math.sqrt)
.filter(x => x % 2 === 1)
.value();
// [1, 3, 5]
因?yàn)槊恳徊椒祷氐亩际前b對(duì)象,所以最后一步的結(jié)果需要調(diào)用value()獲得最終結(jié)果。