本文主要解析jquery链模式的实现,完整的代码在末尾,感谢观看~~
链模式(Operate of Responsibility):通过在对象方法中将当前对象返回,实现对同一个对象多个方法的链式调用。从而简化对该对象的多个方法的多次调用时,对该对象的多次引用...说白了就是可以a().css().attr().on()这样调用方法,而不需要a().css();a().attr();a().on()这样繁琐而且要多次创建对象
jQuery的设计就是通过这种模式使得代码更简洁。这种链模式是基于原型继承的,并且在每一个原型方法的实现上都返回当前对象this,使当前对象一直处于原型链作用域的顶端,这样即可实现链式调用,emm...有点绕,打个比方就是人都能吃饭睡觉打豆豆,有一个模型Person的原型上拥有吃饭、睡觉、打豆豆这三个方法。new Person()创建了一个“我”,“我”吃完饭要接着睡觉,所以吃饭的方法要把“我”丢去睡觉,让“我”去睡觉;“我”睡完觉继续把丢去打豆豆。也就是 “我”.吃饭().睡觉().打豆豆();
要学习链式调用,首先我们需要了解javascript的原型式继承
1.1 原型式继承
var $ = function() {}
$.prototype = {
length: 2,
size: function() {
return this.length;
}
}
var a = new $();
console.log(a.size()); // 2
console.log($.size()); // Uncaught TypeError: $.size is not a function
// 原因:size绑定在$.prototype中而不是在$中
console.log($().size()); // Uncaught TypeError: Cannot read property 'size' of undefined
// 原因:$函数内部没有返回任何值
以上报错的两种方式都是因为size方法绑定在$类的原型上导致的
那么,jQuery中的方法是如何访问的呢?
1.2 找位助手
我们需要找个帮手来实现
var $ = function(selector) {
return B;
}
var B = $.prototype = {
length: 2,
size: function() {
return this.length;
}
}
console.log($().size()); // 2
喜大普奔,我们已经可以正常访问到size方法了,但却创建了一个中间变量B,为了减少变量的创建,我们可以将B看做$的一个属性设置
var $ = function(selector) {
return $.fn;
}
$.fn = $.prototype = {
length: 2,
size: function() {
return this.length;
}
}
console.log($().size()); // 2
1.3 获取元素
以上,我们就能够正常访问对象中的方法了,但却遇到了新的问题,我们需要通过$(selector)获取元素(一组元素族,元素的聚合对象),但现在返回的却是一个$.fn对象,如果$.fn能提供一个获取元素的方法init就好了。所以我们可以将init方法获取到的元素在A方法返回
var $ = function(selector) {
return $.fn.init(selector);
}
$.fn = $.prototype = {
length: 2,
init: function(selector) {
return document.getElementById(selector);
},
size: function() {
return this.length;
}
}
console.log($('demo')); // <div id="demo"></div>
console.log($('demo').size()); // Uncaught TypeError: $(...).size is not a function
1.4 一个大问题
以上的方法虽然能够使我们获取到这个元素,但却无法访问size方法了!!该怎么办呢?
幸运的是,对象中的this指向的就是当前对象,而在init方法中当前的对象就是A.fn,所以我们直接在init中将this返回就OK啦
var $ = function(selector) {
return $.fn.init(selector);
}
$.fn = $.prototype = {
length: 2,
init: function(selector) {
return this;
},
size: function() {
return this.length;
}
}
console.log($('demo').size()); // 2
那么问题来了,我想获取元素,又想访问size方法,该怎么办呢?
机智如我,this不是一个对象吗?对象可以挂在属性呀!我们可以将元素设置为this的一个属性,如果想像数组一样访问,我们可以将他们的属性名顺序的设置成数字索引如this[0],this[1]。为了使它看起来更像数组,我们还可以校正一下它的length属性。
var $ = function(selector) {
return $.fn.init(selector);
}
$.fn = $.prototype = {
length: 2,
init: function(selector) {
this[0] = document.getElementById(selector); // 作为当前对象的属性值保存
this.length = 1;
return this;
},
size: function() {
return this.length;
}
}
var demo = $('demo'); // 记住这个变量
console.log(demo);
console.log($('demo').size());
运行结果
厉害了我的哥,现在我们已经能获取元素而且能够访问size方法了,但是不要高兴的太早,看下面的测试结果
var test = $('test');
console.log('test', test);
console.log('demo', demo);
感觉要凉凉,demo元素被test元素覆盖了!!
1.5 覆盖获取
找了半天,发现导致demo被覆盖的原因是每次在$的构造函数中返回的$.fn.init(selector)对象指向同一个对象。为了解决这个问题,我们可以使用new关键字每次新创建一个对象
var $ = function(selector) {
return new $.fn.init(selector);
}
然鹅...
var demo = $('demo'); // 记住这个变量
var test = $('test');
console.log('test', test);
console.log('demo', demo);
console.log('size', $('demo').size());
1.6 方法丢失
多么痛的领悟,$.fn.init(selector)和new $.fn.init(selector)返回的this不是同一个对象。$.fn.init(selector)返回的this是$.fn也就是$.prototype,而new $.fn.init(selector)的this则指向new创建出来的实例。
init: function(selector) {
this[0] = document.getElementById(selector); // 作为当前对象的属性值保存
this.length = 1;
console.log(this === $.fn, this === $.prototype, this);
return this;
}
测试
$.fn.init('demo')
new $.fn.init('demo')
结果
new 关键字执行时会创建一个对象,并把this指向这一个对象,这个对象能够访问构造函数prototype上的方法
1.7 对比jQuery
打印我们创建的$('demo'),结果如下,__proto__的constructor指向$.fn.init方法
而jQuery创建的$('#demo'),结果如下,__proto__的constructor指向jQuery构造函数
这是因为我们实例化的对象是在执行时创建的,所以constructor指向的就是$.fn.init,为了纠正这一个问题,达到和jQuery一样的效果,我们可以这样做
var $ = function(selector) {
return new $.fn.init(selector);
}
$.fn = $.prototype = {
constructor: $, // 强化构造器
length: 2,
init: function(selector) {
this[0] = document.getElementById(selector);
this.length = 1;
return this;
},
size: function() {
return this.length;
}
}
$.fn.init.prototype = $.fn; // 将构造函数的原型指向一个已存在的对象
测试
console.log($('demo'));
结果,我们发现构造函数已经指向了$,并且__proto__已经存在length和size属性了
1.8 丰富元素获取
以上方案只能够过去一个元素,有时我们需要获取某一类元素,就需要这样改
// selector选择符,context上下文
var $ = function(selector, context) {
return new $.fn.init(selector, context);
}
$.fn = $.prototype = {
constructor: $, // 强化构造器
length: 2,
init: function(selector, context) {
// 获取元素长度
this.length = 0;
// 默认获取元素的上下文为document
context = context || document;
// 如果id是选择符,按位非将-1转换为0,转化为布尔值false
if (~selector.indexOf('#')) {
// 截取id并选择
this[0] = document.getElementById(selector.slice(1));
this.length = 1;
} else { // 如果是元素名称
// 在上下问中选择元素
var doms = context.getElementsByTagName(selector),
i = 0, // 从第一个元素开始筛选
len = doms.length; // 获取元素长度
for(; i < len; i++) {
// 压人this中
this[i] = doms[i];
}
// 校正长度
this.length = len;
}
// 保存上下文
this.context = context;
// 保存选择符
this.selector = selector;
// 返回对象
return this;
},
size: function() {
return this.length;
},
// 增强数组
push: [].push(),
sort: [].sort(),
splice: [].splice()
}
$.fn.init.prototype = $.fn; // 将构造函数的原型指向一个已存在的对象
其中,增强数组是为了让this更像一个数组类型的数据。
javascript是弱类型语言,并且数组、对象、函数都被看成是对象的实例,所以javascript中并没有一个纯粹的数组类型。并且javascript引擎的实现也没有做严格的校验,也是基于一个对象实现的。一些浏览器解析引擎在判断对象是否是数组的时候不仅仅判断其有没有length属性,是否通过'[ 索引值 ]'方式访问元素,还会判断其是否具有数组方法来确定是否要用数组的形式展现,所以我们只需要在$.fn中添加几个数组常用的方法来增强数组特性就可以使this可遍历
测试
console.log($('p'))
1.9 方法拓展
到这里,我们已经将jquery的骨架完成了一大半,我们的目标是实现jquery一样的链式调用,那么jquery是怎么实现链式调用的呢?
原来,jquery中定义了一个extend方法,用于对外部对象扩展和对内部对象扩展。jqueryUI等插件就是通过extend方法进行扩展的。下面,我们就来大展身手实现这个extend方法:如果只有一个参数我们就定义为对$对象或者$.fn对象的扩展,对$.fn对象的扩展是因为我们使用$()返回对象中的方法是从$.fn对象上获取的。多个参数表示对第一个对象的扩展。
// 对象扩展
$.extend = $.fn.extend = function() {
// 扩展对象从第二个参数算起
var i = 1,
len = arguments.length, // 获取参数长度
target = arguments[0], // 第一个参数为源对象
j; // 扩展对象中属性
// 如果只传一个参数
if (i == len) {
target = this; // 源对象为当前对象
i--; // i从0计数
}
// 遍历参数中扩展对象
for (; i < len; i++) {
// 遍历扩展对象中的属性
for (j in arguments[i]) {
target[j] = arguments[i][j]; // 扩展源对象
}
}
// 返回源对象
return target
}
测试
1.10 添加方法
extend方法准备好了,让我们开始向$.fn上添加方法吧~,不要忘了在方法末尾返回this呀~这是链式调用的精髓
$.fn.extend({
// 添加事件,在第一次加载时创建出适用该浏览器的事件绑定方法
on: (function() {
// 标准浏览器DOM2级事件
if (document.addEventListener) {
return function(type, fn) {
var i = this.length -1;
// 遍历所有元素添加事件
for (; i >=0; i--) {
this[i].addEventListener(type, fn, false);
}
// 返回对象
return this;
}
// IE浏览器DOM2级事件
} else if (document.attachEvent) {
return function(type, fn) {
var i = this.length -1;
// 遍历所有元素添加事件
for (; i >=0; i--) {
this[i].addEvent('on' + type, fn);
}
// 返回对象
return this;
}
} else {
return function(type, fn) {
var i = this.length -1;
// 遍历所有元素添加事件
for (; i >=0; i--) {
this[i]['on' + type] = fn;
}
// 返回对象
return this;
}
}
})()
});
$.extend({
// 将‘-’分割线转化为驼峰式,如'border-color'->'borderColor'
camelCase: function(str) {
return str.replace(/\-(\w)/g, function(all, letter) {
return letter.toUpperCase();
});
}
});
$.fn.extend({
// 设置css样式,如果只传一个参数,参数是字符串则返回第一个css样式值,此时不能进行链式调用。
// 如果是对象则为每一个元素设置多个样式
// 如果是两个参数则为每一个元素设置样式
css: function() {
var arg = arguments,
len = arg.length;
if (this.length < 1) {
return this;
}
// 只有一个参数时
if (len === 1) {
// 如果为字符串则获取第一个元素CSS样式
if (typeof arg[0] === 'string') {
// IE
if (this[0].currentStyle) {
return this[0].currentStyle[name];
} else {
return getComputedStyle(this[0], false)[name];
}
// 为对象时则设置多个样式
} else if (typeof arg[0] === 'object') {
for (var i in arg[0]) { // 遍历每个样式
for (var j = this.length - 1; j >= 0; j--) {
// 调用扩展方法camelCase将'-'分割线转化为驼峰式
this[j].style[$.camelCase(i)] = arg[0][i];
}
}
}
// 两个参数设置每个元素样式
} else if (len === 2) {
for (var j = this.length - 1; j >= 0; j--) {
this[j].style[$.camelCase(arg[0])] = arg[1];
}
}
// 返回对象
return this;
}
});
$.fn.extend({
// 设置属性
attr: function() {
var arg = arguments,
len = arg.length;
if (this.length < 1) {
return this;
}
// 只有一个参数时
if (len === 1) {
// 如果为字符串则获取第一个元素属性
if (typeof arg[0] === 'string') {
return this[0].getAttribute(arg[0]);
// 为对象时则设置每个元素的多个属性
} else if (typeof arg[0] === 'object') {
for (var i in arg[0]) { // 遍历属性
for (var j = this.length - 1; j >= 0; j--) {
// 调用扩展方法camelCase将'-'分割线转化为驼峰式
this[j].setAttribute(i, arg[0][i]);
}
}
}
// 两个参数设置每个元素单个属性
} else if (len === 2) {
for (var j = this.length - 1; j >= 0; j--) {
this[j].setAttribute(arg[0], arg[1]);
}
}
// 返回对象
return this;
}
});
$.fn.extend({
// 获取或设置元素内容
html: function() {
var arg = arguments,
len = arg.length;
// 无参数则获取第一个元素的内容
if (len === 0) {
return this[0] && this[0].innerHTML;
// 一个参数则设置每一个元素的内容
} else {
for (var i = this.length - 1; i >= 0; i--) {
this[i].innerHTML = arg[0];
}
}
// 返回对象
return this;
}
});
测试
$('#demo')
.css({
height: '50px',
border: '1px solid #000',
'background-color': 'red'
})
.attr('class', 'demo')
.html('add demo text')
.on('click', function() {
console.log('clicked');
});
大功告成!!
总结
附完整代码
// selector选择符,context上下文
var $ = function(selector, context) {
return new $.fn.init(selector, context);
}
$.fn = $.prototype = {
constructor: $, // 强化构造器
length: 2,
init: function(selector, context) {
// 获取元素长度
this.length = 0;
// 默认获取元素的上下文为document
context = context || document;
// 如果id是选择符,按位非将-1转换为0,转化为布尔值false
if (~selector.indexOf('#')) {
// 截取id并选择
this[0] = document.getElementById(selector.slice(1));
this.length = 1;
} else { // 如果是元素名称
// 在上下问中选择元素
var doms = context.getElementsByTagName(selector),
i = 0, // 从第一个元素开始筛选
len = doms.length; // 获取元素长度
for(; i < len; i++) {
// 压人this中
this[i] = doms[i];
}
// 校正长度
this.length = len;
}
// 保存上下文
this.context = context;
// 保存选择符
this.selector = selector;
// 返回对象
return this;
},
size: function() {
return this.length;
},
// 增强数组
push: [].push(),
sort: [].sort(),
splice: [].splice()
}
// 对象扩展
$.extend = $.fn.extend = function() {
// 扩展对象从第二个参数算起
var i = 1,
len = arguments.length, // 获取参数长度
target = arguments[0], // 第一个参数为源对象
j; // 扩展对象中属性
// 如果只传一个参数
if (i == len) {
target = this; // 源对象为当前对象
i--; // i从0计数
}
// 遍历参数中扩展对象
for (; i < len; i++) {
// 遍历扩展对象中的属性
for (j in arguments[i]) {
target[j] = arguments[i][j]; // 扩展源对象
}
}
// 返回源对象
return target
}
$.fn.extend({
// 添加事件,在第一次加载时创建出适用该浏览器的事件绑定方法
on: (function() {
// 标准浏览器DOM2级事件
if (document.addEventListener) {
return function(type, fn) {
var i = this.length -1;
// 遍历所有元素添加事件
for (; i >=0; i--) {
this[i].addEventListener(type, fn, false);
}
// 返回对象
return this;
}
// IE浏览器DOM2级事件
} else if (document.attachEvent) {
return function(type, fn) {
var i = this.length -1;
// 遍历所有元素添加事件
for (; i >=0; i--) {
this[i].addEvent('on' + type, fn);
}
// 返回对象
return this;
}
} else {
return function(type, fn) {
var i = this.length -1;
// 遍历所有元素添加事件
for (; i >=0; i--) {
this[i]['on' + type] = fn;
}
// 返回对象
return this;
}
}
})()
});
$.extend({
// 将‘-’分割线转化为驼峰式,如'border-color'->'borderColor'
camelCase: function(str) {
return str.replace(/\-(\w)/g, function(all, letter) {
return letter.toUpperCase();
});
}
});
$.fn.extend({
// 设置css样式,如果只传一个参数,参数是字符串则返回第一个css样式值,此时不能进行链式调用。
// 如果是对象则为每一个元素设置多个样式
// 如果是两个参数则为每一个元素设置样式
css: function() {
var arg = arguments,
len = arg.length;
if (this.length < 1) {
return this;
}
// 只有一个参数时
if (len === 1) {
// 如果为字符串则获取第一个元素CSS样式
if (typeof arg[0] === 'string') {
// IE
if (this[0].currentStyle) {
return this[0].currentStyle[name];
} else {
return getComputedStyle(this[0], false)[name];
}
// 为对象时则设置多个样式
} else if (typeof arg[0] === 'object') {
for (var i in arg[0]) { // 遍历每个样式
for (var j = this.length - 1; j >= 0; j--) {
// 调用扩展方法camelCase将'-'分割线转化为驼峰式
this[j].style[$.camelCase(i)] = arg[0][i];
}
}
}
// 两个参数设置每个元素样式
} else if (len === 2) {
for (var j = this.length - 1; j >= 0; j--) {
this[j].style[$.camelCase(arg[0])] = arg[1];
}
}
// 返回对象
return this;
}
});
$.fn.extend({
// 设置属性
attr: function() {
var arg = arguments,
len = arg.length;
if (this.length < 1) {
return this;
}
// 只有一个参数时
if (len === 1) {
// 如果为字符串则获取第一个元素属性
if (typeof arg[0] === 'string') {
return this[0].getAttribute(arg[0]);
// 为对象时则设置每个元素的多个属性
} else if (typeof arg[0] === 'object') {
for (var i in arg[0]) { // 遍历属性
for (var j = this.length - 1; j >= 0; j--) {
// 调用扩展方法camelCase将'-'分割线转化为驼峰式
this[j].setAttribute(i, arg[0][i]);
}
}
}
// 两个参数设置每个元素单个属性
} else if (len === 2) {
for (var j = this.length - 1; j >= 0; j--) {
this[j].setAttribute(arg[0], arg[1]);
}
}
// 返回对象
return this;
}
});
$.fn.extend({
// 获取或设置元素内容
html: function() {
var arg = arguments,
len = arg.length;
// 无参数则获取第一个元素的内容
if (len === 0) {
return this[0] && this[0].innerHTML;
// 一个参数则设置每一个元素的内容
} else {
for (var i = this.length - 1; i >= 0; i--) {
this[i].innerHTML = arg[0];
}
}
// 返回对象
return this;
}
});
$.fn.init.prototype = $.fn; // 将构造函数的原型指向一个已存在的对象