链模式
通常情况下,通过对构造函数使用 new 会返回一个绑定到 this 上的新实例,所以我们可以在 new 出来的对象上直接用 . 访问其属性和方法。如果在普通函数中也返回当前实例,那么我们就可以使用 . 在单行代码中一次性连续调用多个方法,就好像它们被链接在一起一样,这就是链式调用,又称链模式。
之前建造者模式、组合模式等文章已经用到了链模式,日常使用的 jQuery、Promise 等也使用了链模式,我们对使用形式已经很熟悉了,下面一起来看看链模式的原理。
- 什么是链模式 1.1 链模式的实现
在 jQuery 时代,下面这样的用法我们很熟悉了:
// 使用链模式
$('div')
.show()
.addClass('active')
.height('100px')
.css('color', 'red')
.on('click', function(e) {
// ...
})
源码中的链模式 3.1 jQuery 中的链模式 1. jQuery 构造函数
jQuery 方法看似复杂,可以简写如下:
var jQuery = function(selector, context) {
// jQuery 方法返回的是 jQuery.fn.init 所 new 出来的对象
return new jQuery.fn.init(selector, context, rootjQuery)
}
jQuery.fn = jQuery.prototype = {
constructor: jQuery,
// jQuery 对象的构造函数
init: function(selector, context, rootjQuery) {
// ... 一顿匹配操作,返回一个拼装好的伪数组的自身实例
// 是 jQuery.fn.init 的实例,也就是我们常用的 jQuery 对象
return this
},
selector: '',
eq: function() { ... },
end: function() { ... },
map: function() { ... },
last: function() { ... },
first: function() { ... },
// ... 其他方法
}
jQuery.fn.init 的实例都拥有 jQuery.fn 相应的方法jQuery.fn.init.prototype = jQuery.fn
// 此处源码位于 src/core.jsreturn new jQuery.fn.init(...) 这句看似复杂,其实也就是下面的这个 init 方法,
这个方法最后返回的是我们常用的 jQuery 对象,下面还有一句 jQuery.fn.init.prototype = jQuery.fn,
因此最上面的 jQuery 方法返回的 new 出来的 jQuery.fn.init 实例将继承 jQuery.fn` 上的方法:
const p = $("<p/>")
$.fn === p.__proto__ // true
因此返回出来的实例也将继承 eq、end、map、last 等 jQuery.fn 上的方法。
1、jQuery 实例方法
下面我们一起看看,show、hide、toggle 这些方法是如何实现链模式的呢 :
jQuery.fn.extend({
show: function() {
var elem
for (i = 0; i < this.length; i++) {
// ...
elem = this[i]
if (elem.style.display === 'none') {
elem.style.display = 'block'
}
}
return this
},
hide: function() { ... },
toggle: function() { ... }
})
这里首先使用了一个方法 jQuery.fn.extend(),简单看一下这个方法做啥的:
jQuery.extend = jQuery.fn.extend = function(options) {
// ... 一系列啰啰嗦嗦的判断
for (name in options) {
this[name] = options[ name ] // 此处 this === jQuery.fn
}
}
// 此处源码位于 src/core.js
这个方法就是把传参的对象的值赋值给 jQuery.fn
,因为这时候这个方法是通过上下文对象 jQuery.fn.extend()
方式来调用,属于隐式绑定。(对 this 绑定规则的同学参看本专栏第 2 篇文章)
以 show
方法为例,此时这个方法被赋到 jQuery.fn
对象上,而通过上文我们知道,jQuery.fn.init.prototype = jQuery.fn
,而jQuery.fn.init
这个方法是作为构造函数被 jQuery
函数 new
出来并返回,因此 show
方法此时可以被jQuery.fn.init
实例访问到,也就可以被$('selector')
访问到,因此此时我们已经可以: $('p').show()
了。
那么我们再回头来看看show
方法的实现,show
方法将匹配的元素的 display
置为 block
之后返回了 this
。注意了,此时的 this
也是隐式绑定,而且是通过$('p')
点出来的,因此返回的值就是$('p')
的引用。
经过以上步骤,我们知道 show
方法返回的仍然是 $('p')
的引用,我们可以继续在之后点出来其他 jQuery.fn
对象上的方法,css、hide、toggle、addClasson
等等方法同理,至此,jQuery
的链模式就形成了。
2、Underscore 中的链模式
如果你用过 Underscore
,那么你可能知道 Underscore
提供的一个链模式实现 _.chain
。通过这个方法,可以方便地使用Underscore
提供的一些方法链模式地对数据进行处理。另外,Lodash
的chain
实现和 Underscore
的基本一样,可以自行去Lodash
的 GitHub
仓库阅读。
比如这里我们需要对一个用户对象数组进行一系列操作,首先按年龄排序,去掉年龄为奇数的人,再将这些用户的名字列成数组:
var users = [
{ 'name': 'barney', 'age': 26 },
{ 'name': 'fred', 'age': 21 },
{ 'name': 'pebbles', 'age': 28 },
{ 'name': 'negolas', 'age': 23 }
]
_.chain(users)
.sortBy('age')
.reject(user => user.age % 2)
.map(user => user.name)
.value()
// 输出: ["barney", "pebbles"]
经过 _.chain
方法处理后,就可以使用 Underscore 提供的其他方法对这个数据进行操作,下面一起来看看源码是如何实现链模式。
首先是 _.chain
方法:
_.chain = function(obj) {
var instance = _(obj) // 获得一个经 underscore 包裹后的实例
instance._chain = true // 标记是否使用链式操作
return instance
}
此处源码位于 underscore.js#L1615-L1619
这里通过_(obj)
的方式把数据进行了包装,并返回了一个对象,结构如下:
{
_chain: true,
_wrapped: [...],
__proto__: ...
}
返回的对象的隐式原型可以访问到
Undersocre
提供的很多方法,如下图:
这个
chain
方法的作用就是创建一个包裹了obj
的Underscore
实例对象,并标记该实例是使用链模式,最后返回这个包装好的链式化实例(叫链式化是因为可以继续调用underscore
上的方法)。
我们一起看看 sort
方法是如何实现的:
var chainResult = function (instance, obj) {
return instance._chain ? _(obj).chain() : obj; // 这里 _chain 为 true
};
_.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
var method = Array.prototype[name];
_.prototype[name] = function() {
var obj = this._wrapped;
method.apply(obj, arguments); // 执行方法
return chainResult(this, obj);
};
});
此处源码位于
underscore.js#L1649-L1657
-
sort
方法执行之后,把结果重新放在_wrapped
里,并执行chainResult
方法,这个方法里由于_chain
之前已经置为true
,因此会继续对结果调用chain()
方法,包装成链式化实例并返回。 - 最后的这个
_.value
方法比较简单,就是返回链式化实例的_wrapped
值:
_.prototype.value = function() {
return this._wrapped;
};
此处源码位于
underscore.js#L1668-L1670
总结一下,只要一开始调用了
chain
方法,_chain
这个标志位就会被置为true
,在类似的方法中,返回的值都用chainResult
包裹一遍,并判断这个_chain
这个标志位,为true
则返回链式化实例,供给下一次方法调用,由此形成了链式化调用