任何复杂的技术都是从最简单的问题开始的,如果你被 jQuery 几千行庞杂结构的源代码所困惑,那么建议你阅读本节内容,我们将探索 jQuery 是如何从最简单的问题开始,并逐步实现羽翼渐丰的演变过程,从 jQuery 核心技术的还原过程来理解 jQuery 框架的搭建原理。
用过 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>
当我们为 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 类的实例。因此,下面这种结构模型才是正确的。
那么如何返回一个 jQuery 实例呢?
回忆一下,当使用 var my$ = new $(); 创建 jQuery 类的实例时,this 关键字就指向对象 my$ ,因此 my$ 实例对象就获得了 jQuery.prototype 包含的原型属性或方法,这些方法内的 this 关键字就会自动指向 my$ 实例对象。换句话说,this 关键字总是指向类的实例。
因此,我们可以这样尝试:在 jQuery 中使用一个工厂方法来创建一个实例 (就是 jQuery.fn),把这个方法放在 jQuery.prototype 原型对象中,然后在 jQuery() 函数中返回这个原型方法的调用。代码如下所示。
我们已经初步实现了让 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 new jQuery.fn.init(); // 实例化 init 初始化类型,分隔作用域
};
</script>
这样就可以把 init() 构造器中的 this 和 jQuery.fn 对象中的 this 关键字隔离开来,避免相互混淆。但是,这种方式也会带来另一个问题:无法访问 jQuery.fn 对象的属性或方法。例如,在下面的示例中,访问 jQuery.fn 原型对象的 jquery 属性和 size() 方法时就会出现这个问题。
如何做到既能够分隔初始化构造器函数与 jQuery 原型对象的作用域,又能够在返回实例中访问 jQuery 原型对象呢?
jQuery 框架巧妙地通过原型传递解决了这个问题,它把 jQuery.fn 传递给 jQuery.fn.init.prototype ,也就是说用 jQuery 的原型对象覆盖 init 构造器的原型对象,从而实现跨域访问,其代码如下所示。
这是一招妙棋,new jQuery.fn.init() 创建的新对象拥有 init 构造器的 prototype 原型对象的方法,通过改变 prototype 指针的指向,使其指向 jQuery 类的 prototype ,这样创建出来的对象就继承了 jQuery.fn 原型对象定义的方法。
jQuery 返回的是 jQuery 对象,jQuery 对象是一个类数组的对象,本质上它就是一个对象,但是它拥有数组的长度和下标,却没有继承数组的方法。
很显然,上面几节的讲解都是建立在一种空理论基础上的,目的是希望读者能够理解 jQuery 框架的核心构建过程。下面,我们就尝试为 jQuery() 函数传递一个参数,并让它返回一个 jQuery 对象。
jQuery() 函数包含两个参数 selector 和 context ,其中 selector 表示选择器,而 context 表示选择的内容范围,它表示一个 DOM 元素。为了简化操作,我们假设选择器的类型仅限定为标签选择器。实现的代码如下所示。