任何复杂的技术都是从最简单的问题开始的,如果你被 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 对象集合的长度。