underscore之内部函数cb和optimizeCb

原文来自冴羽

_.map

_.map 类似于 Array.prototype.map,但是更加健壮和完善,下面是 _.map 的源码

1
2
3
4
5
6
7
8
9
10
// 简化过,这里仅假设obj是数组
_.map = function(obj, iteratee, context) {
iteratee = cb(iteratee, context);
// Array(1) -> [empty] | Array(2) -> [empty * 2] | Array(1,2, ...) -> [1, 2 , ..]
var length = obj.length, result = Array(length);
for(var index = 0; index < length; index ++) {
results[index] = iteratee(obj[index], index, obj);
}
return results;
}

map方法除了传入要处理的数组之外,还有两个参数 iteratee 和 context, 类似于 Array.prototype.map中的其他两个参数,其中iteratee表示处理函数, context表示指定的执行上下文,即 this的值。

然后在源码中,我们看到,我们将iteratee 和 context 传入一个cb函数,然后覆盖掉iteratee函数,然后将这个函数用作最终的处理函数。

实际上,需要这么麻烦吗?这里只是使用了iteratee函数处理每次迭代的值,通过context指定this的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_.map = function(obj, iteratee, context) {
var length = obj.length, results = Array(length);

for(var index = 0; index < length; index++) {
results[index] = iteratee.call(context, obj[index], index, obj);
}
return results;
}

// [2, 3, 4]
console.log(_.map([1, 2, 3]), function(item){
return item + 1;
})

// [2, 3, 4]
console.log(_.map([1, 2, 3]), function(item) {
return item = this.value;
}, {value: 1})

这样写,程序并不健壮,万一iteratee传入的不是一个函数呢?比如传入一个null,或者一个对象,字符串、数字。。。
使用上面的方法就会报错,那在underscore中呢?

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
 //使用underscore
//什么也不传
var result = _.map([1,2,3]); // [1, 2, 3]

// 传入一个对象
var result = _.map([{name: 'blue'}, {name: 'yellow', age: 18}], {name: 'green'}); [false, true];

var result = _.map([{name: 'blue'}, {name: 'yellow'}], 'name'); //['blue', 'yellow']


// underscore 的源码
var cb = function(value, context, argCount) {
if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);
if (value == null) return _.identity;
if (_.isFunction(value)) return optimizeCb(value, context, argCount);
if (_.isObject(value) && !_.isArray(value)) return _.matcher(value);
return _.property(value);
};

_.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;
};

我们会发现,underscore尽然还能根据传入的值的类型不同,实现的效果不同。

  1. 当iteratee不传时,返回一个相同的数组。
  2. 当iteratee为一个函数,正常处理
  3. 当iteratee为一个对象,返回元素是否匹配指定的对象
  4. 当iteratee为字符串,返回元素对应的属性值的集合。

由此我们可以推测处underscore的cb函数中,有对iteratee值类型的判断,然后根据不同的类型,返回不同iteratee函数。

cb

来看看cb的源码

1
2
3
4
5
6
7
var cb = function(value, context, argCount) {
if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);
if (value == null) return _.identity;
if (_.isFunction(value)) return optimizeCb(value, context, argCount);
if (_.isObject(value) && !_.isArray(value)) return _.matcher(value);
return _.property(value);
};

这里又使用到了其他函数,我们一个一个来看下

_.iteratee
1
2
3
_.iteratee = builtinIteratee = function(value, context) {
return cb(value, context, Infinity);
};

因为 .iteratee中 `.iteratee = builtinInteratee,所以在cb中 第一个判断 条件值为false,也就是说return _.iteratee(value, context)`,正常情况下是不会执行的.

但是如果我们在外部修改了 .iteratee函数,结果便会为true,cb函数直接返回 `.iteratee(value, context)`

这个意思其实是说我们自定义的_.iteratee函数来处理value 和 context。

试想我们并不需要现在 _.map这么强大的功能,我们只希望value是一个函数,就用该函数处理数组元素,如果不是函数,就直接返回当前元素,我们可以这样修改。

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
_.iteratee = function(value, context) {
if(typeof value === 'function') {
return function(...rest) {
return value.call(context, ...rest);
};
}

// 不是函数直接返回value
return function(value) {
return value;
};
}

// 使用underscore库 实现下面两个效果,还需要对underscore源码的shallowProperty 进行一定的修改,才能出现结果
var shallowProperty = function(key) {
return function(s, currentkey, obj) {
return obj == null ? void 0 : obj[currentkey];
};
};



// 如果map的第二个参数不是函数,就返回该元素
console.log(_.map([1, 2, 3], 'name')); // [1, 2, 3]

// 如果map的第二个参数是函数,就是用函数处理数组元素
var result = _.map([1, 2, 3], function(item) {
return item + 1;
})
console.log(result); // [2,3,4]

当然更多的情况是自定义对不同的value使用不同的处理函数,值得注意的是,underscore中的许多函数都使用到了cb函数,而cb函数又使用了 _.iteratee函数,如果你修改这个函数,会影响到多个函数,这些函数基本都属于集合函数,具体包括 map, find, filter, reject, every, some, max, min, sortBy, groupBy, indexBy, countBy, sortedIndex, partition, 和 unique

_.identity
1
2
3
4
// Keep the identity function around for default iteratees.
_.identity = function(value) {
return value;
};

这也就是为什么当map的第二个参数什么都不传的时候,结果会是一个相同数组的原因。

optimizeCb

当value是一个函数的时候,就传入optinmizeCb函数。
if (_.isFunction(value)) return optimizeCb(value, context, argCount);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Internal function that returns an efficient (for current engines) version
// of the passed-in callback, to be repeatedly applied in other Underscore
// functions.
var optimizeCb = function(func, context, argCount) {
if (context === void 0) return func;
switch (argCount == null ? 3 : argCount) {
case 1: return function(value) {
return func.call(context, value);
};
// The 2-argument case is omitted because we’re not using it.
case 3: return function(value, index, collection) {
return func.call(context, value, index, collection);
};
case 4: return function(accumulator, value, index, collection) {
return func.call(context, accumulator, value, index, collection);
};
}
return function() {
return func.apply(context, arguments);
};
};

也许你会好奇,为什么我要对 argCount进行判断呢?就不能直接返回吗?比如:

1
2
3
4
5
6
7
var optimizeCb = function(func, context) {
//如果没有传入 context 就返回 func函数
if(context === void 0) return func;
return function() {
return func.apply(context, arguments);
}
}

当然没有问题,但是为什么underscore要这样做呢? 其实就是为了避免使用 arguments,提高一点性能而已,如果不是写一个库,其实还真没必要做到这点。

而为什么当参数是3个时候,参数名分别是 value, index, collection, 又为什么没有参数2的情况呢?其实这都是更具underscore函数用到的情况,因为没有函数用到了两个参数,于是就省略了,像map函数就会用到3个参数,就根据这三个参数的名字起了这里的变量名。

_.matcher

if (_.isObject(value) && !_.isArray(value)) return _.matcher(value);

这段就是用来处理当map的第二个参数是对象的情况:

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
48
49
50
51
52
53
54
55
// Returns a predicate for checking whether an object has a given set of
// `key:value` pairs.
_.matcher = _.matches = function(attrs) {
attrs = _.extendOwn({}, attrs);
return function(obj) {
return _.isMatch(obj, attrs);
};
};

// Assigns a given object with all the own properties in the passed-in object(s).
// (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)
_.extendOwn = _.assign = createAssigner(_.keys);

// Retrieve the names of an object's own properties.
// Delegates to **ECMAScript 5**'s native `Object.keys`.
_.keys = function(obj) {
if (!_.isObject(obj)) return [];
if (nativeKeys) return nativeKeys(obj);
var keys = [];
for (var key in obj) if (has(obj, key)) keys.push(key);
// Ahem, IE < 9.
if (hasEnumBug) collectNonEnumProps(obj, keys);
return keys;
};

// An internal function for creating assigner functions.
var createAssigner = function(keysFunc, defaults) { // keysFunc == _.keys
return function(obj) { // == _.extendOwn
var length = arguments.length;
if (defaults) obj = Object(obj);
if (length < 2 || obj == null) return obj;
for (var index = 1; index < length; index++) {
var source = arguments[index],
keys = keysFunc(source),
l = keys.length;
for (var i = 0; i < l; i++) {
var key = keys[i];
if (!defaults || obj[key] === void 0) obj[key] = source[key];
}
}
return obj;
};
};

// Returns whether an object has a given set of `key:value` pairs.
_.isMatch = function(object, attrs) {
var keys = _.keys(attrs), length = keys.length;
if (object == null) return !length;
var obj = Object(object);
for (var i = 0; i < length; i++) {
var key = keys[i];
if (attrs[key] !== obj[key] || !(key in obj)) return false;
}
return true;
};
_.property

return _.property(value);

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
  // Creates a function that, when passed an object, will traverse that object’s
// properties down the given `path`, specified as an array of keys or indexes.
_.property = function(path) {
// 如果不是数组
if (!_.isArray(path)) {
return shallowProperty(path);
}
return function(obj) {
return deepGet(obj, path);
};
};

var shallowProperty = function(key) {
return function(obj) {
return obj == null ? void 0 : obj[key];
};
};

// 根据路径取出深层次的值
var deepGet = function(obj, path) {
var length = path.length;
for (var i = 0; i < length; i++) {
if (obj == null) return void 0;
obj = obj[path[i]];
}
return length ? obj : void 0;
};

原来value还可以传一个数组,用来取深层次的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var person1 = {
child: {
nickName: 'blue'
}
}

var person2 = {
child: {
nickName: 'yellow'
}
}

var result = _.map([person1,person2], ['child', 'nickName']);
console.log(result) // ['blue', 'yellow']