第二章 jQuery技术解密(一)

2.2 jQuery 原型技术分解

任何复杂的技术都是从最简单的问题开始的,如果你被 jQuery 几千行庞杂结构的源代码所困惑,那么建议你阅读本节内容,我们将探索 jQuery 是如何从最简单的问题开始,并逐步实现羽翼渐丰的演变过程,从 jQuery  核心技术的还原过程来理解 jQuery 框架的搭建原理。

2.2.1 起源 -- 原型继承

用过 JavaScript 的读者都会明白,在 JavaScript 脚本中到处都是函数,函数可以归置代码段,把相对独立的功能封装在一个函数包中。函数也可以实现类,这个类是面向对象编程中最基本的概念,也是最高抽象,定义一个类就相当于制作了一个模型,然后借助这个模型复制无数的实例。

例如,下面的代码就可以定义最初的 jQuery 类,类名就是 jQuery ,你可以把它视为一个函数,函数名是 jQuery 。当然,你也可以把它视为一个对象,对象名就是 jQuery 。与其他面向对象的编程语言相比,JavaScript 对于这个概念的界定好像很随意,这降低了编程的门槛,反之也降低了 JavaScript 作为编程语言的层次。

<script language="javascript" type="text/javascript">

var jQuery = function(){

// 函数体

};

</script>

上面创建了一个空的函数,好像什么都不能够做,这个函数实际上就是所谓的构造函数。构造函数在面向对象语言中是类的一个特殊方法,用来创建类。在 JavaScript 中,你可以把任何函数都视为构造函数,这没有什么不可以的,这样不会伤害代码本身。

所有类都有最基本的功能,如继承、派生和重写等。JavaScript 很奇特,它通过为所有函数绑定一个 prototype 属性,由这个属性指向一个原型对象,原型对象中可以定义类的继承属性和方法等。所以,对于上面的空类,可以继续扩展原型,其代码如下。

<script language="javascript" type="text/javascript">

var jQuery = function(){};

jQuery.prototype = {

// 扩展的原型对象

};

</script>

原型对象是 JavaScript 实现继承的基本机制。 如果你觉得 jQuery.prototype 名称太长,没有关系,我们可以为其重新命名,如 fn ,当然你可以随便命名。如果直接命名 fn ,则表示该名称属性 Window 对象,即全局变量名。更安全的方法是为 jQuery  类定义一个公共属性, jQuery.fn ,然后把 jQuery 的原型对象传递给这个公共属性,实现代码如下。

<script language="javascript" type="text/javascript">

jQuery.fn = jQuery.prototype = {

// 扩展的原型对象

};

</script>

这里的 jQuery.fn 相当于 jQuery.prototype 的别名,方便以后使用,它们指向同一个引用。 因此若要调用 jQuery 的原型方法,直接使用 jQuery.fn 公共属性即可,不需要直接引用 jQuery.prototype ,当然直接使用 jQuery.prototype 也是可以的。

既然原型对象可以使用别名,jQuery 类也可以起个别名,我们可以使用 $ 符号来引用它,代码如下。

var $ = jQuery = function(){};

现在模仿 jQuery 框架源码,给它添加两个成员,一个是原型属性 jquery ,一个是原型方法 size(),其代码如下。

<script language="javascript" type="text/javascript">

var $ = jQuery = function(){};

jQuery.fn = jQuery.prototype = {

jquery: "1.3.2",          // 原型属性

size: function(){ // 原型方法

return this.length;

}

};

</script>

2.2.2 生命 -- 返回实例

当我们为 jQuery 添加了两个原型成员:jquery 属性和 size() 方法之后,这个框架最基本的样子就孕育出来了。但是该如何调用 jquery 属性和 size() 方法呢?

也许,你可以采用如下方法调用:

<script language="javascript" type="text/javascript">

var my$ = new $();            // 实例化

alert(my$.jquery); // 调用属性,返回 "1.3.2"

alert(my$.size());// 调用方法,返回 undefined

</script>

但是,jQuery 不是这样调用的。它模仿类似下面的方法进行调用。

$().jquery;

$().size();

也就是说,jQuery 没有使用 new 运算符将 jQuery 类实例化,而是直接调用 jQuery() 函数,然后在这个函数后面直接调用 jQuery 的原型方法。这是怎么实现的呢?

如果你模仿 jQuery 框架的用法执行下面的代码,浏览器会显示编译错误。这说明上面这个案例代码还不是真正的 jQuery 技术原型。

alert($().jquery);

alert($().size());

也就是说,我们应该把 jQuery 看做一个类,同时也应该把它视为一个普通函数,并让这个函数的返回值为 jQuery 类的实例。因此,下面这种结构模型才是正确的。

<script type="text/javascript">
var $ = jQuery = function(){
	return new jQuery();      // 返回类的实例
};

jQuery.fn = jQuery.prototype = {
	jquery: "1.3.2",          // 原型属性
	size: function(){ // 原型方法
		return this.length;
	}
};

alert($().jquery);
alert($().size());
</script>
但是,如果在浏览器中预览,则会提示如图 2.1 所示的错误。内存外溢,说明出现了死循环引用。


那么如何返回一个 jQuery 实例呢?

回忆一下,当使用 var my$ = new $(); 创建 jQuery 类的实例时,this 关键字就指向对象 my$ ,因此 my$ 实例对象就获得了 jQuery.prototype 包含的原型属性或方法,这些方法内的 this 关键字就会自动指向 my$ 实例对象。换句话说,this 关键字总是指向类的实例。

因此,我们可以这样尝试:在 jQuery 中使用一个工厂方法来创建一个实例 (就是 jQuery.fn),把这个方法放在 jQuery.prototype 原型对象中,然后在 jQuery() 函数中返回这个原型方法的调用。代码如下所示。

<script type="text/javascript">
var $ = jQuery = function(){
	return jQuery.fn.init();		// 调用原型 init()
};

jQuery.fn = jQuery.prototype = {
	init: function(){				// 在初始化原型方法中返回实例的引用
		return this;
	},
	jquery: "1.3.2",          		// 原型属性
	size: function(){ 				// 原型方法
		return this.length;
	}
};

alert($().jquery);        			// 调用属性,返回 "1.3.2"
alert($().size());					// 调用方法,返回 undefined
</script>

2.2.3 学步 -- 分隔作用域

我们已经初步实现了让 jQuery() 函数能够返回 jQuery 类的实例,下面继续思考:init() 方法返回的是 this 关键字,该关键字引用的是 jQuery 类的实例,如果在 init() 函数中继续使用 this 关键字,也就是说,假设我们把 init() 函数也视为一个构造器,则其中的 this 该如何理解和处理?

例如,在下面示例中,jQuery 原型对象中包含一个 length 属性,同时 init() 从一个普通的函数转身变成了构造器,它也包含一个 length 属性和一个 test() 方法。运行该示例,我们可以看到,this 关键字引用了 init() 函数作用域所在的对象, 此时它访问 length 属性时,返回0. 而 this 关键字也能够访问上一级对象 jQuery.fn 对象的作用域,所以 $().jquery 返回 "1.3.2" 。但是调用 $().size() 方法时,返回的是 0, 而不是 1 。

<script type="text/javascript">
var $ = jQuery = function(){
	return jQuery.fn.init();		// 调用原型 init()
};

jQuery.fn = jQuery.prototype = {
	init: function(){				// 在初始化原型方法中返回实例的引用
		this.length = 0;
		this.test = function(){
			return this.length;
		}
		return this;
	},
	jquery: "1.3.2",          		// 原型属性
	length: 1,
	size: function(){ 				// 原型方法
		return this.length;
	}
};

alert($().jquery);			// 返回 "1.3.2"
alert($().test());			// 返回 0
alert($().size());			// 返回 0
</script>
这种设计思路很容易破坏作用域的独立性,对于 jQuery 这样的框架来说,很可能会造成消极影响。因此,我们可以看到 jQuery 框架是通过下面的方式调用 init() 初始化构造函数的。

<script type="text/javascript">

var $ = jQuery = function(){

return new jQuery.fn.init();                      // 实例化 init 初始化类型,分隔作用域

};

</script>

这样就可以把 init() 构造器中的 this 和 jQuery.fn 对象中的 this 关键字隔离开来,避免相互混淆。但是,这种方式也会带来另一个问题:无法访问 jQuery.fn 对象的属性或方法。例如,在下面的示例中,访问 jQuery.fn 原型对象的 jquery 属性和 size() 方法时就会出现这个问题。

<script type="text/javascript">
var $ = jQuery = function(){
	return new jQuery.fn.init();		// 实例化 init 初始化类型,分隔作用域
};

jQuery.fn = jQuery.prototype = {
	init: function(){				// 在初始化原型方法中返回实例的引用
		this.length = 0;
		this.test = function(){
			return this.length;
		}
		return this;
	},
	jquery: "1.3.2",          		// 原型属性
	length: 1,
	size: function(){ 				// 原型方法
		return this.length;
	}
};

alert($().jquery);			// 返回 undefined
alert($().test());			// 返回 0
alert($().size());			// 抛出异常
</script>

2.2.4 生长 -- 跨域访问

如何做到既能够分隔初始化构造器函数与 jQuery 原型对象的作用域,又能够在返回实例中访问 jQuery 原型对象呢?

jQuery 框架巧妙地通过原型传递解决了这个问题,它把 jQuery.fn 传递给 jQuery.fn.init.prototype ,也就是说用 jQuery 的原型对象覆盖 init 构造器的原型对象,从而实现跨域访问,其代码如下所示。

<script type="text/javascript">
var $ = jQuery = function(){
	return new jQuery.fn.init();		// 实例化 init 初始化类型,分隔作用域
};

jQuery.fn = jQuery.prototype = {
	init: function(){				// 在初始化原型方法中返回实例的引用
		this.length = 0;
		this.test = function(){
			return this.length;
		}
		return this;
	},
	jquery: "1.3.2",          		// 原型属性
	length: 1,
	size: function(){ 				// 原型方法
		return this.length;
	}
};

jQuery.fn.init.prototype = jQuery.fn; // 使用 jQuery 的原型对象覆盖 init 的原型对象
alert($().jquery);					// 返回 "1.3.2"
alert($().test());					// 返回 0
alert($().size());					// 返回 0
</script>

这是一招妙棋,new jQuery.fn.init() 创建的新对象拥有 init 构造器的 prototype 原型对象的方法,通过改变 prototype 指针的指向,使其指向  jQuery 类的 prototype ,这样创建出来的对象就继承了 jQuery.fn 原型对象定义的方法。

2.2.5 成熟 -- 选择器

jQuery 返回的是 jQuery 对象,jQuery 对象是一个类数组的对象,本质上它就是一个对象,但是它拥有数组的长度和下标,却没有继承数组的方法。

很显然,上面几节的讲解都是建立在一种空理论基础上的,目的是希望读者能够理解 jQuery 框架的核心构建过程。下面,我们就尝试为 jQuery()  函数传递一个参数,并让它返回一个 jQuery 对象。

jQuery() 函数包含两个参数 selector 和 context ,其中 selector 表示选择器,而 context 表示选择的内容范围,它表示一个 DOM 元素。为了简化操作,我们假设选择器的类型仅限定为标签选择器。实现的代码如下所示。

<div></div>
<div></div>
<div></div>
<script type="text/javascript">
var $ = jQuery = function(selector, context){			// 定义类      
	return new jQuery.fn.init(selector, context);		// 返回选择器的实例
};

jQuery.fn = jQuery.prototype = {				// jQuery 类的原型对象
	init: function(selector, context){			// 定义选择器构造器
		selector = selector || document;		// 设置默认值为 document
		context = context || document;			// 设置默认值为 document
		
		if(selector.nodeType){					// 如果选择符为节点对象
			this[0] = selector;					// 把参数节点传递给实例对象的数组
			this.length = 1;					// 并设置实例对象的 length 属性,定义包含的元素个数
			this.context = selector;			// 设置实例的属性,返回选择范围
			return this;						// 返回当前实例
		}
		if(typeof selector === "string"){		// 如果选择符是字符串
			var e = context.getElementsByTagName(selector);	// 获取指定名称的元素
			for(var i = 0; i<e.length; i++){	// 遍历元素集合,并把所有元素填入到当前实例数组中
				this[i] = e[i];
			}
			this.length = e.length;				// 设置实例的 length 属性,即定义包含的元素个数
			this.context = context;				// 设置实例的属性,返回选择范围
			return this;						// 返回当前实例
		} else {
			this.length = 0;					// 否则,设置实例的 length 属性值为 0
			this.context = context;				// 设置实例的属性,返回选择范围
			return this;						// 返回当前实例
		}
	},
	jquery: "1.3.2",          		// 原型属性
	size: function(){ 				// 原型方法
		return this.length;
	}
};

jQuery.fn.init.prototype = jQuery.fn; // 使用 jQuery 的原型对象覆盖 init 的原型对象
alert($("div").size());					// 返回 3
</script>
在上面示例中,$("div") 基本拥有了 jQuery 框架中 $("div") 语法的功能,使用它可以选取页面中指定范围的 div 元素。同时,调用 size() 方法可以返回 jQuery 对象集合的长度。
  • 1
    点赞
  • 0
    收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值