本篇文章参考书籍《JavaScript设计模式》–张容铭
前言
大家一定用过 JQuery 吧,这东西的一大特性就是链式调用,获取到某个元素后,直接连续执行多个方法,贼帅,其实我们也可以做到。
本节开始,就给大家介绍一些技巧性的设计模式,平时能经常用到,类似语法糖一样,简单易懂,而且功能奇妙,不会像前面的设计模式那样长篇大论,肯定比前面容易接收,而且有趣。
链模式
通过对象方法中,将当前对象返回,实现对同一个对象多个方法的链式调用。
大家看一下下面的代码:
var A = function() {
}
A.prototype = {
length: 2,
size: function() {
return this.length;
}
}
我们创建了一个对象 A , 并且 A 的原型对象上拥有一个属性和一个 size 方法,那么我想访问这个 size 方法,应该怎么做?
方法A在原型上,可以通过关键字 new 创建对象访问,如下:
var a = new A();
consoele.log(a.size()); //2
上面是正确的访问方法,如果按照下面的方式访问,程序就会报错。
console.log(A.size());
console.log(A().size());
上面第一种报错是因为, size 是绑在 A 的原型上的,而不是 A 上,第二种方式报错是因为执行完 A 后,没有返回值,所以找不到 size 方法。但是在 JQuery 中就可以访问到。
JQuery 是通过 $() 的方式进行链式调用的,说明在 $() 函数执行结束后,返回了一个但有很多方法的对象。原理弄清楚了,接下来实现一下。
var A = function() {
return A.fn;
}
A.fn = A.prototype = {
length: 2,
size: function() {
return this.length;
}
}
到这里会遇到一个新问题,我们知道 JQuery 的目的是为了获取元素,返回的是一组元素簇(元素的聚合对象),但是现在返回的却是一个 A.fn 对象,显然达不到我们的需求,所以如果 A.fn 能提供给我们一个获取元素的方法 init 就好了,我们可以将 init 方法获取到的元素在 A 方法中返回。
var A = function(selector) {
return A.fn.init(selector);
}
A.fn = A.prototype = {
init: function(selector) {
//将元素所谓一个属性赋值给fn
this[0] = document.getElementById(selector);
//矫正length属性
this.length = 1;
return this;
},
length: 2,
size: function() {
return this.length;
}
}
var demo = A('demo');
console.log(demo); //Object{0: div#demo, init: function, length: 1, size: function}
console.log(A('demo').size()) //1
上面的代码还是有问题,当我们再获取一个 id 不为 demo 的元素的时候,后面会覆盖掉前面的代码。这是因为对象是引用数据类型,解决这个问题也很容易,使用 关键字 new 就可以。
但是这又会导致调用 size 方法的时候报错。
这个问题是因为我们通过 new 对对象内的属性复制了, this 的指向就不是 A.fn 了而是 A.fn.A.init 。
为什么this会是 A.fn.A.init ?
这个问题就要从构造函数说起了,各位听我慢慢道来哈~
我们知道 new 关键字执行的实质是对构造函数的属性进行一次复制,那么 new A.fn.init(selector) 的构造函数就可以表示成 A.fn.init = A.init ,我们将 A.init 带入到 A.fn.init 中的 init ,就可以得到 A.fn.A.init 的结果了。
回到我们上面遇到的那个问题,在使用 new 关键字了之后,this不再是 A.fn 了那我们怎么解决一下?
这个问题 JQuery 中有一个很巧妙的解决方案,只要将构造函数的原型指向一个已经存在的对象即可。
A.fn.init.prototype = A.fn;
这是因为实例化的对象是在构造函数执行时创建的,所以 constructor 指向的就是 A.fn.A.init 构造函数,但是这个对象在执行完毕之后就不存在了,所以我们为了强化 constructor 可以按照如下做法:
var A = function(selector) {
return new A.fn.init(selector);
}
A.fn = A.prototype = {
//强化构造器
constructor: A,
init: function(selector) {
console.log(this.constructor)
/**
* 输出结果
* fucntion(selector) {
* return new A.fn.init(selector);
* }
*/
...
}
...
}
A.fn.init.prototype = A.fn;
现在 A 框架的 _ _ proto _ _ 为 A 了, size 方法也能正常获得了。
获取一类元素
上面我们能获取带有 id 元素的链式方法了,现在我们尝试获取某一类元素,别的不用动,只需要改 init 方法就行。
//selector 选择符, context 上下文
var A = function(selector, context) {
return new A.fn.init(selector, context);
}
A.fn = A.prototype = {
constructor: A,
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;
},
...
}
数组与对象
如果大伙研究过 JQ 的源码,会发现, JQ 获取的元素更像一个数组,而我们的 A 框架返回的却是一个对象,这是由于 JS 是若语言类型,数组,函数,对象,都是对象的实例,所以是没有纯粹的数组类型的,浏览器在判断是否是数组的时候会判断 length 属性,能否通过 [索引值] 访问,是否有数组方法等。
所以我们可以给 A.fn 中增加几个数组方法,来欺骗浏览器。
A.fn = A.prototype = {
//...
//增强数组
push: [].push,
sort: [].sort,
splice: [].splice
}
方法拓展
JQ 中 很多方法都可以通过点语法链式使用,那这些方法我们因该怎么扩展呢?
JQ 的做法是定义了一个 extend 方法, jQueryUI 就是通过它拓展的,有时我们对对象拓展也会用到它,所以 extend 有两个作用,一是外部对下那个拓展,二是内部对象拓展。根据这个原理,我们可以简单实现 extend 方法:
如果只有一个参数我们就定义对 A 对象或者 A.fn 的拓展,对 A.fn 的拓展是因为我们使用 A() 返回对象中的方法是从 A.fn 上获取的。多个参数表示对第一个对象的拓展。
//对象拓展
A.extend = A.fn.extend = function() {
//拓展对象从第二个参数算起
var i = 1,
len = arguments.length,
//第一个参数为源对象
target = arguments[0],
//拓展对象中属性
j;
//如果只传一个参数
if(i == len) {
//源对象为当前对象
target = this;
//i从0计数
i--;
}
//遍历参数中拓展对象
for(; i < len; i++) {
//遍历拓展对象中的属性
for(j in arguments[i]) {
//拓展源对象
target[j] = arguments[i][j];
}
}
//返回源对象
return target;
}
//测试
//拓展一个对象
var demo = A.extend({
first: 1}, {
second: 2}, {
third: 3});
console.log(demo); //{first: 1, second: 2, third: 3}
//拓展 A.fn 方式一
A.extend(A.fn, {
version: '1.0'});
console.log(A('demo').version) //1.0
//拓展 A.fn 方式二
A.fn.extend({
getVersion: function() {
return this.version;}});
console.log(A('demo').getVersion()) //1.0
//拓展 A 方式一
A.extend(A, {
author: '不见星空'});
console.log(A.author) //不见星空
//拓展 A 方式二
A.extend({
nickName: '不见星空'});
console.log(A.nickName) //不见星空
添加方法
接下来我们正式的为 A 框架添加元素绑定事件 on ,设置 CSS 方法,设置元素属性方法 attr ,设置元素内容方法 html 。
A.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;
}
//不支持DOM2级事件浏览器添加事件
} else {
return function(type, fn) {
var i = this.length - 1;
for(; i >= 0; i--) {
this[i]['on' + type] = fn;
}
return this;
}
}
}) ()
})
获取或设置 css 样式方法中,如果只传递一个参数,如果参数是字符串,则返回第一个元素 css 样式值,此时不能进行链式调用。如果是对象则为每一个元素设置多个 css 样式,如果是两个参数则为每一个元素设置样式。
A.extend({
//设置css样式
css: function() {
var arg = arguments,
len = arg,length;
if(this.length < 1) {
return this;
}
//只有一个参数时
if(len === 1) {
//如果为字符串则为获取第一个元素 CSS 样式
if(type 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[A.camelCase(arg[0])] = arg[0][i];
}
}
}
//两个参数设置一个样式
} else if(len === 2) {
for(var j = this.length - 1; j >= 0; j--) {
this[j].style[A.camelCase(arg[0])] = arg[1];
}
}
return this;
}
})
获取元素属性的方法与设置 css 样式方法一样,如果只传一个参数,如果参数为字符串,则返回第一个元素属性值,此时不能再链式调用,如果参数是对象则设置每一个元素的多个属性值,如果传递两个参数,则第一个参数为属性名,第二个参数为属性值,设置每个元素的属性。
A.fn.extend({
//设置属性
attr: function() {
var arg = arguments,
len = arg.lenght;
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--) {
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;
}
})
获取或这只内容的方法参数跟前两个有些变化,如果无参数则返回第一个元素的内容,此时亦不能链式调用,如果有参数,则将第一个参数作为内容来设置各个元素内容。
A.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;
}
})
大功告成,不容易不容易,赶紧来测试一下我们自己的链式操作。
A('div')
.css({
height: '30px',
width: '40px'
})
.attr('class', 'demo')
.html('add demo text')
.on('click', function() {
console.log('clicked');
});
是不是很有趣,原理弄懂了,按照思路一步一步弄很容易。