Collection Functions

Each

line: 165
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_.each = _.forEach = function(obj, iteratee, context) {
iteratee = optimizeCb(iteratee, context);
var i, length;
if (isArrayLike(obj)) {
for (i = 0, length = obj.length; i < length; i++) {
iteratee(obj[i], i, obj);
}
} else {
var keys = _.keys(obj);
for (i = 0, length = keys.length; i < length; i++) {
iteratee(obj[keys[i]], keys[i], obj);
}
}
return obj;
};

先透過optimizeCb將傳入的迭代器作優化

再來透過 isArrayLike判斷物件為Array或者是Object

再分別用不同的方式迭代

Array 可以直接透過index, Object則要先取得各個key value pair的key去做迭代

並套用先前優化過的迭代器

Map

line: 182
1
2
3
4
5
6
7
8
9
10
11
_.map = _.collect = function(obj, iteratee, context) {
iteratee = cb(iteratee, context);
var keys = !isArrayLike(obj) && _.keys(obj),
length = (keys || obj).length,
results = Array(length);
for (var index = 0; index < length; index++) {
var currentKey = keys ? keys[index] : index;
results[index] = iteratee(obj[currentKey], currentKey, obj);
}
return results;
};

這段程式碼一開始是先透過cb取得相對應的callback function

透過cb,optimizeCb這兩個function

iteratee 應該會變成這樣接受3個參數的function

1
iteratee(value, index, collection)

接下來是要遍歷整個Object或者Array並且透過iteratee來做迭代

並將迭代的值傳入results

1
2
3
4
5
6
var keys = !isArrayLike(obj) && _.keys(obj),
// 如果不是Array 就取得所有的key
length = (keys || obj).length,
// 取得Array 和 ArrayOf(keys)的長度
results = Array(length);
// 建立一個長度為length的空Array

createReduce

line 195
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var createReduce = function(dir) {
// Wrap code that reassigns argument variables in a separate function than
// the one that accesses `arguments.length` to avoid a perf hit. (#1991)
var reducer = function(obj, iteratee, memo, initial) {
var keys = !isArrayLike(obj) && _.keys(obj),
length = (keys || obj).length,
index = dir > 0 ? 0 : length - 1;
if (!initial) {
memo = obj[keys ? keys[index] : index];
index += dir;
}
for (; index >= 0 && index < length; index += dir) {
var currentKey = keys ? keys[index] : index;
memo = iteratee(memo, obj[currentKey], currentKey, obj);
}
return memo;
};
return function(obj, iteratee, memo, context) {
var initial = arguments.length >= 3;
return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial);
};
};

createReduce這個function執行後會回傳一個匿名function

可以接受4個參數

這個function在arguments小於三的時候要進行initialize

而reducer的部分

一開始是和_.map一樣取得length

這裡不一樣的是reduce有方向

若是dir = 1則是從左邊開始 此時index = 0

dir = -1則是從右邊開始 此時 index = length - 1

memo是先暫存當前index的值也就是 obj[0 || length-1]

再來就是把index指向memo的下一個位置 不論是向左reduce或者向右reduce

接者進到for迴圈當中進行reduce

for迴圈內部的iteratee 是經過optimizeCb所優化的callback function

1
function iteratee(accumulator, value, index, collection);

而accumulator就是我們的memo當前的值

透過這樣的方式就可以建立一個reduce function

Reduce

line: 221
1
2
3
_.reduce = _.foldl = _.inject = createReduce(1);
_.reduceRight = _.foldr = createReduce(-1);

所有的reduce function都可以透過createReduce來產生

Find

要使用_.find 之前要先看 createPredicateIndexFinder

line: 629
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var createPredicateIndexFinder = function(dir) {
return function(array, predicate, context) {
predicate = cb(predicate, context);
var length = getLength(array);
var index = dir > 0 ? 0 : length - 1;
for (; index >= 0 && index < length; index += dir) {
if (predicate(array[index], index, array)) return index;
}
return -1;
};
};
_.findIndex = createPredicateIndexFinder(1);
_.findLastIndex = createPredicateIndexFinder(-1);

透過createPredicateIndexFinder來產生 .findIndex,.findLastIndex這兩個函式

predicate(判斷函示)其實也就是個回傳true,false的function

createPredicateIndexFinder會先將predicate丟到cb裡

由於predicate是個函式

所以會透過optimizeCb去做優化

再來就是遍歷整個Array來找到回傳true的值時的index

並回傳出去

結束整個find的過程

再來則是針對Object的findKey

line: 1070
1
2
3
4
5
6
7
8
_.findKey = function(obj, predicate, context) {
predicate = cb(predicate, context);
var keys = _.keys(obj), key;
for (var i = 0, length = keys.length; i < length; i++) {
key = keys[i];
if (predicate(obj[key], key, obj)) return key;
}
};

大致上的邏輯和先前許多例子差不多

line: 227
1
2
3
4
5
_.find = _.detect = function(obj, predicate, context) {
var keyFinder = isArrayLike(obj) ? _.findIndex : _.findKey;
var key = keyFinder(obj, predicate, context);
if (key !== void 0 && key !== -1) return obj[key];
};

在看完上面的findIndex跟findKey

之後大概就可以知道find就是將Array的find 跟 Object的 findKey集合在一起的函式

並透過obj[key]來得到value

Filter

line: 235
1
2
3
4
5
6
7
8
9
10
11
12
13
_.filter = _.select = function(obj, predicate, context) {
var results = [];
predicate = cb(predicate, context);
_.each(obj, function(value, index, list) {
if (predicate(value, index, list)) results.push(value);
});
return results;
};
// Return all the elements for which a truth test fails.
_.reject = function(obj, predicate, context) {
return _.filter(obj, _.negate(cb(predicate)), context);
};

filter將傳入的obj透過_.each遍歷一遍

並將predicate函式執行為true的值push到results內

最後回傳results

而reject則是透過_.negate取得相反的結果

line: 839
1
2
3
4
5
_.negate = function(predicate) {
return function() {
return !predicate.apply(this, arguments);
};
};

Every

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
_.every = _.all = function(obj, predicate, context) {
predicate = cb(predicate, context);
var keys = !isArrayLike(obj) && _.keys(obj),
length = (keys || obj).length;
for (var index = 0; index < length; index++) {
var currentKey = keys ? keys[index] : index;
if (!predicate(obj[currentKey], currentKey, obj)) return false;
}
return true;
};
// Determine if at least one element in the object matches a truth test.
// Aliased as `any`.
_.some = _.any = function(obj, predicate, context) {
predicate = cb(predicate, context);
var keys = !isArrayLike(obj) && _.keys(obj),
length = (keys || obj).length;
for (var index = 0; index < length; index++) {
var currentKey = keys ? keys[index] : index;
if (predicate(obj[currentKey], currentKey, obj)) return true;
}
return false;
};

這幾個函式分別是檢測Obj能否通過predicate判斷

包含全部都通過 .every = .all

部分通過 .some = .any

Contains

line:277
1
2
3
4
5
_.contains = _.includes = _.include = function(obj, item, fromIndex, guard) {
if (!isArrayLike(obj)) obj = _.values(obj);
if (typeof fromIndex != 'number' || guard) fromIndex = 0;
return _.indexOf(obj, item, fromIndex) >= 0;
};

單純就是檢驗Array,Object內有沒有某個item

第一個if是將Object的所有value變成一個ArrayLikeObj

Invoke

line: 284
1
2
3
4
5
6
7
_.invoke = restArgs(function(obj, method, args) {
var isFunc = _.isFunction(method);
return _.map(obj, function(value) {
var func = isFunc ? method : value[method];
return func == null ? func : func.apply(value, args);
});
});

_.invoke是透過restArgs來呼叫

這邊就可以透過很好的例子來理解restArgs到底是做什麼用的

先透過_.isFunction來判斷method是否是一個function

再來透過_.map去遍歷obj中所有的物件

接著若isFunc是真就代表method本身就是個function

否則就去試著用value[mehtod]取得遍歷的物件的method

若都沒有則回傳null

最後舉個例子

1
2
3
var Arr = [[1,2,3,4,5],[2,3,4,1,5,6],[1,5,3,3,6,3,2]];
_.invoke(Arr,'slice',0,2); //[[1, 2], [2, 3], [1, 5]]

由於這幾行程式碼的關係

restArgs
1
2
3
4
5
6
7
8
9
/* ... */
startIndex = startIndex == null ? func.length - 1 : +startIndex;
switch (startIndex) {
case 0: return func.call(this, rest);
case 1: return func.call(this, arguments[0], rest);
case 2: return func.call(this, arguments[0], arguments[1], rest);
}
/* ... */
invoke
1
_.invoke = restArgs(function(obj, method, args) {

這裡的func.length-1等於2

所以restArgs會將 最後的兩的args 0,2變成Array

所以map出來的function會變成func.apply(value, [0,2])

讓剩餘的函數可以做更好的操作

Convenience version of XXX

line: 293
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Convenience version of a common use case of `map`: fetching a property.
_.pluck = function(obj, key) {
return _.map(obj, _.property(key));
};
// Convenience version of a common use case of `filter`: selecting only objects
// containing specific `key:value` pairs.
_.where = function(obj, attrs) {
return _.filter(obj, _.matcher(attrs));
};
// Convenience version of a common use case of `find`: getting the first object
// containing specific `key:value` pairs.
_.findWhere = function(obj, attrs) {
return _.find(obj, _.matcher(attrs));
};

Max & Min

line: 310
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
_.max = function(obj, iteratee, context) {
var result = -Infinity, lastComputed = -Infinity,
value, computed;
if (iteratee == null || (typeof iteratee == 'number' && typeof obj[0] != 'object') && obj != null) {
obj = isArrayLike(obj) ? obj : _.values(obj);
for (var i = 0, length = obj.length; i < length; i++) {
value = obj[i];
if (value != null && value > result) {
result = value;
}
}
} else {
iteratee = cb(iteratee, context);
_.each(obj, function(v, index, list) {
computed = iteratee(v, index, list);
if (computed > lastComputed || computed === -Infinity && result === -Infinity) {
result = v;
lastComputed = computed;
}
});
}
return result;
};
_.min = function(obj, iteratee, context) {
var result = Infinity, lastComputed = Infinity,
value, computed;
if (iteratee == null || (typeof iteratee == 'number' && typeof obj[0] != 'object') && obj != null) {
obj = isArrayLike(obj) ? obj : _.values(obj);
for (var i = 0, length = obj.length; i < length; i++) {
value = obj[i];
if (value != null && value < result) {
result = value;
}
}
} else {
iteratee = cb(iteratee, context);
_.each(obj, function(v, index, list) {
computed = iteratee(v, index, list);
if (computed < lastComputed || computed === Infinity && result === Infinity) {
result = v;
lastComputed = computed;
}
});
}
return result;
};

.max 跟 .min可以單純比較數值的大小

也能透過iteratee去迭代後比較大小

並且回傳原來的值

sample & shuffle

line: 360
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
_.shuffle = function(obj) {
return _.sample(obj, Infinity);
};
_.sample = function(obj, n, guard) {
if (n == null || guard) {
if (!isArrayLike(obj)) obj = _.values(obj);
return obj[_.random(obj.length - 1)];
}
var sample = isArrayLike(obj) ? _.clone(obj) : _.values(obj);
var length = getLength(sample);
n = Math.max(Math.min(n, length), 0);
var last = length - 1;
for (var index = 0; index < n; index++) {
var rand = _.random(index, last);
var temp = sample[index];
sample[index] = sample[rand];
sample[rand] = temp;
}
return sample.slice(0, n);
};

Sample 是透過 Fisher-Yates shuffle 實作

從obj中取出隨機的n個值

_.sample 接受三個參數 obj, n, guard

若n === null 或者有gurad 則回傳obj中隨機的一個值

首先 sample 先複製一份一模一樣的值存在Array當中

n 為指定的smaple數量 若大於obj長度 則以obj長度為n

for迴圈中跑的就是Fisher-Yates shffle

最後return 時透過slice達到sample的效果

因此_.shuffle的實作就非常簡單

只要將n指定為Infinity就可以了

sortBy

line: 387
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
_.sortBy = function(obj, iteratee, context) {
var index = 0;
iteratee = cb(iteratee, context);
return _.pluck(_.map(obj, function(value, key, list) {
return {
value: value,
index: index++,
criteria: iteratee(value, key, list)
};
}).sort(function(left, right) {
var a = left.criteria;
var b = right.criteria;
if (a !== b) {
if (a > b || a === void 0) return 1;
if (a < b || b === void 0) return -1;
}
return left.index - right.index;
}), 'value');
};

這段程式碼呼叫了兩個underscore的function

和一個javascript內建的sort function

先從map來看

將Obj內的element先map到一個新的object

1
2
3
4
5
{
value: value, // Object內的Element 可以是任何js的型別
indes: index++, // 從0遞增的index
criteria: iteratee(value, key, list) // 符合iteratee所回傳的值,也是我們要用來比較的依據
}

Object中的iteratee也是透過 cb 來回傳的iteratee

因此可以是一個function,object或者是一個字串

以字串為例子

1
2
iteratee = cb('name', context);
// iteratee 就等於 _.property('name')

所以criteria存的值就會是value的name property

在map完之後透過sort去自行定義一個compareFunction

並去比較criteria

藉此完成sorting

但此時sorting完的Array我們mapping完的格式

因此可以透過_.pluck的方式將value property的值取出來

得到我們要的結果

Group

line: 408
1
2
3
4
5
6
7
8
9
10
11
var group = function(behavior, partition) {
return function(obj, iteratee, context) {
var result = partition ? [[], []] : {};
iteratee = cb(iteratee, context);
_.each(obj, function(value, index) {
var key = iteratee(value, index, obj);
behavior(result, value, key);
});
return result;
};
};

先看group function

這是個underscore 內部使用的function

用來產生.groupBy, .indexBy, _.countBy 等function

group接受兩個參數 behavior, partition

並且回傳一個function

而behavior簡單來說就是 依照相對應的value跟key

我要怎麼去產生result

所以group的邏輯就是

回傳一個function可以接受三個參數 obj, iteratee, context

跟之前一樣透過 cb 產生相對應的 iteratee

透過_.each去遍歷obj中的每一個物件

利用iteratee去產生key

透過behavior去產生不同的result

接下來看不同的behavior可以產生什麼樣的result

line: 422
1
2
3
_.groupBy = group(function(result, value, key) {
if (_.has(result, key)) result[key].push(value); else result[key] = [value];
});

第一個_.groupBy 的behavior很簡單

透過 _.has來檢驗result有沒有相對應的key

若有則直接push value到相對應key底下的array

若沒有則產生一個新的key 並將 [value] 指定在該key底下

1
2
3
_.indexBy = group(function(result, value, key) {
result[key] = value;
});

再來 _.indexBy

直接產生一個新的key 並將 [value] 指定在該key底下

因此要先確定key都是唯一的

1
2
3
_.countBy = group(function(result, value, key) {
if (_.has(result, key)) result[key]++; else result[key] = 1;
});

第三個 _.countBy

透過 _.has來檢驗result有沒有相對應的key

若有直接將相對應的key的value + 1

若沒有則產生一個新的key 並將 value 初始成1

1
2
3
_.partition = group(function(result, value, pass) {
result[pass ? 0 : 1].push(value);
}, true);

最後一個 _.partition (二分法)

在前面介紹underscore group function的時候跳過了一段

就是

1
var result = partition ? [[], []] : {};

在大部分情況下我們做Gourping都會回傳一個Obj

而_.partition 就是一個例外的情況

在這個情況下我們的結果會是一個Array 裡面有兩的Array 的Element

透過iteratee去判斷輸入obj是否通過

藉此將value分別push到不同的Array當中