最近又跟着冴羽大大的博客阅读了underscore的源码,为了加深自己的理解,梳理了一遍学习的重点,之后打算写underscore源码阅读系列文章,那么就进入正文吧!
在了解underscore是如何实现链式调用的之前,我们先来看一下underscore中函数调用的两种方式。
const arr = [1, 2, 3]
//函数式风格
_.map(arr, item => item * 2)
//面向对象风格
_(arr).map(item => item * 2)
实现函数式与面向对象两种调用方式
思考一下,我们该怎么同时实现这两种调用方式?
- 实现函数式调用很直观,我们可以定义
var _ = {}
,将函数直接挂载在字面量空对象上。 - 既然以
_([1, 2, 3])
的形式可以执行,就说明_
是一个函数对象,而非普通字面量对象。因此,我们可以定义
var _ = function(){}
_.map = () => { // function code }
但问题在于,如何做到_([1, 2, 3]).map(...)
?也就是说,_([1, 2, 3])
返回的结果怎么才能调用挂载在_
函数上的方法呢?我们来看underscore的解决方式:
var _ = function (obj) {
if (obj instanceof _) return obj;
if (!this instanceOf _) return new _(obj)
this._wrapped = obj
}
分析_([1, 2, 3])
这段代码的执行过程:
1.首次调用_函数, obj instanceof _
显然为假,继续执行
2.this
指向全局对象,if语句判断为真,执行new _(obj)
3.对于new操作符,其工作过程可以理解为:
function _() {}
var f = new _()
等价于
function _() {}
function newFunc(_, ...args) {
var newobj = {}
newobj.__proto__ = _.prototype
var res = _.call(newobj, ...args)
return isObject(res) ? res : newobj
}
var f = newFfunc(_)
因此执行new _(obj)
过程中,当执行var res = _.call(newobj, ...args
)语句时,将调用_函数,其this实际指向newobj,而newobj.__proto__ = _.prototype
,因此_函数中的if判断语句为假,继续执行this.wrapped = obj
,最后返回var res = undefined
,因此newFunc(_)最后返回了newobj对象,即{_wrapped: [1, 2, 3]}
,该对象的原型指向_.prototype
。
但目前函数只是挂载在_函数对象上,并没有挂载在_.prototype上,_([1, 2, 3])
返回的实例也无法调用_函数对象上所挂载的方法。因此,underscore通过mixin将_函数对象上的所有方法复制到_.prototype上。
_.functions
首先,通过_.functions
函数获取挂载在_上的所有方法。
// Return a sorted list of the function names available on the object.
_.functions(obj) {
var names = [];
for (var key in obj) {
if (isFunction(obj[key])) names.push(key);
}
return names.sort();
}
isFunction细节暂且不谈,总之是用来判断所传参数是否为一个函数。
mixin
function mixin(obj) {
//_.functions返回所传对象上的所有函数名
_.each(_.functions(obj), name => {
var func = _[name] = obj[name]
_.prototype[name] = function() {
var args = [this._wrapped];
Array.prototype.push.apply(args, arguments);
return func.apply(_, args)
};
})
return _
}
到目前为止,我们已经实现了函数式与面向对象两种调用方式。接下来,我们实现链式调用。
链式调用
在underscore中默认不使用链式调用,但有需要时也可以通过_.chain函数实现。例如:
_.filter = function (arr, callback, context) {
return arr.filter(callback, context)
}
_.map = function (arr, callback, context) {
return arr.map(callback, context)
}
_.chain([1, 2, 3])
.filter(num => num % 2 === 0)
.map(num => num * num)
.value(); // [4]
我们来看_.chain函数做了什么。
_.chain = function (obj) {
var instance = _(obj);
instance._chain = true;
return instance;
}
_.chain([1, 2, 3])
内部通过面向对象风格调用了_,返回了一个对象为 {_wrapped: [1,2,3], _chain: true}
,该对象原型指向_.prototype。_.chain([1,2,3]).filter(num => num % 2 === 0)
调用了通过mixin后挂载在_.prototype上的filter方法,但该方法返回值为[2],而不是返回一个原型指向_.prototype的实例对象,因此无法继续调用其他方法。
你可能会想将返回值再传入_.chain()函数,不就可以继续调用了吗?比如:
var res = _.chain([1,2,3]).filter(num => num % 2 === 0)
var res2 = _.chain(res).map(num => num * num)
var res3 = _.chain(res2).otherfunc(...)
没错,但是这个重复_.chain()的过程是不是可以通过某种方式自动实现?
那么具体怎么做呢?我们可以通过函数调用对象._chain
判断是否为链式调用,进而判断是返回一个原型指向_.prototype的实例对象还是返回obj本身。
实现方式如下:
var chainResult = function(instance, obj) {
return instance._chain ? _.chain(obj) : obj
}
function mixin(obj) {
//_.functions返回所传对象上的所有函数名
_.each(_.functions(obj), name => {
var func = _[name] = obj[name]
_.prototype[name] = function() {
var args = [this._wrapped];
Array.prototype.push.apply(args, arguments);
return chainResult(this, func.apply(_, args))
};
})
return _
}
_.prototype.value = function () {
return this._wrapped;
};
到此为止,我们已经完全实现了链式调用。在文章的最后,我们来做一个小测试吧,链式调用你真的学会了吗?
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。