代码要写成别人看不懂的样子(十五)

本篇文章参考书籍《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');
});

  是不是很有趣,原理弄懂了,按照思路一步一步弄很容易。



猜你喜欢

转载自blog.csdn.net/EcbJS/article/details/110887745