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 类的实例。因此,下面这种结构模型才是正确的。

[html] view plaincopy
  1. <script type="text/javascript">  
  2. var $ = jQuery = function(){  
  3.     return new jQuery();      // 返回类的实例  
  4. };  
  5.   
  6. jQuery.fn = jQuery.prototype = {  
  7.     jquery: "1.3.2",          // 原型属性  
  8.     size: function(){ // 原型方法  
  9.         return this.length;  
  10.     }  
  11. };  
  12.   
  13. alert($().jquery);  
  14. alert($().size());  
  15. </script>  
但是,如果在浏览器中预览,则会提示如图 2.1 所示的错误。内存外溢,说明出现了死循环引用。


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

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

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

[html] view plaincopy
  1. <script type="text/javascript">  
  2. var $ = jQuery = function(){  
  3.     return jQuery.fn.init();        // 调用原型 init()  
  4. };  
  5.   
  6. jQuery.fn = jQuery.prototype = {  
  7.     init: function(){               // 在初始化原型方法中返回实例的引用  
  8.         return this;  
  9.     },  
  10.     jquery: "1.3.2",                // 原型属性  
  11.     size: function(){               // 原型方法  
  12.         return this.length;  
  13.     }  
  14. };  
  15.   
  16. alert($().jquery);                  // 调用属性,返回 "1.3.2"  
  17. alert($().size());                  // 调用方法,返回 undefined  
  18. </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 。

[html] view plaincopy
  1. <script type="text/javascript">  
  2. var $ = jQuery = function(){  
  3.     return jQuery.fn.init();        // 调用原型 init()  
  4. };  
  5.   
  6. jQuery.fn = jQuery.prototype = {  
  7.     init: function(){               // 在初始化原型方法中返回实例的引用  
  8.         this.length = 0;  
  9.         this.test = function(){  
  10.             return this.length;  
  11.         }  
  12.         return this;  
  13.     },  
  14.     jquery: "1.3.2",                // 原型属性  
  15.     length: 1,  
  16.     size: function(){               // 原型方法  
  17.         return this.length;  
  18.     }  
  19. };  
  20.   
  21. alert($().jquery);          // 返回 "1.3.2"  
  22. alert($().test());          // 返回 0  
  23. alert($().size());          // 返回 0  
  24. </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() 方法时就会出现这个问题。

[html] view plaincopy
  1. <script type="text/javascript">  
  2. var $ = jQuery = function(){  
  3.     return new jQuery.fn.init();        // 实例化 init 初始化类型,分隔作用域  
  4. };  
  5.   
  6. jQuery.fn = jQuery.prototype = {  
  7.     init: function(){               // 在初始化原型方法中返回实例的引用  
  8.         this.length = 0;  
  9.         this.test = function(){  
  10.             return this.length;  
  11.         }  
  12.         return this;  
  13.     },  
  14.     jquery: "1.3.2",                // 原型属性  
  15.     length: 1,  
  16.     size: function(){               // 原型方法  
  17.         return this.length;  
  18.     }  
  19. };  
  20.   
  21. alert($().jquery);          // 返回 undefined  
  22. alert($().test());          // 返回 0  
  23. alert($().size());          // 抛出异常  
  24. </script>  

2.2.4 生长 -- 跨域访问

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

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

[html] view plaincopy
  1. <script type="text/javascript">  
  2. var $ = jQuery = function(){  
  3.     return new jQuery.fn.init();        // 实例化 init 初始化类型,分隔作用域  
  4. };  
  5.   
  6. jQuery.fn = jQuery.prototype = {  
  7.     init: function(){               // 在初始化原型方法中返回实例的引用  
  8.         this.length = 0;  
  9.         this.test = function(){  
  10.             return this.length;  
  11.         }  
  12.         return this;  
  13.     },  
  14.     jquery: "1.3.2",                // 原型属性  
  15.     length: 1,  
  16.     size: function(){               // 原型方法  
  17.         return this.length;  
  18.     }  
  19. };  
  20.   
  21. jQuery.fn.init.prototype = jQuery.fn; // 使用 jQuery 的原型对象覆盖 init 的原型对象  
  22. alert($().jquery);                  // 返回 "1.3.2"  
  23. alert($().test());                  // 返回 0  
  24. alert($().size());                  // 返回 0  
  25. </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 元素。为了简化操作,我们假设选择器的类型仅限定为标签选择器。实现的代码如下所示。

[html] view plaincopy
  1. <div></div>  
  2. <div></div>  
  3. <div></div>  
  4. <script type="text/javascript">  
  5. var $ = jQuery = function(selector, context){           // 定义类        
  6.     return new jQuery.fn.init(selector, context);       // 返回选择器的实例  
  7. };  
  8.   
  9. jQuery.fn = jQuery.prototype = {                // jQuery 类的原型对象  
  10.     init: function(selector, context){          // 定义选择器构造器  
  11.         selector = selector || document;        // 设置默认值为 document  
  12.         context = context || document;          // 设置默认值为 document  
  13.           
  14.         if(selector.nodeType){                  // 如果选择符为节点对象  
  15.             this[0] = selector;                 // 把参数节点传递给实例对象的数组  
  16.             this.length = 1;                    // 并设置实例对象的 length 属性,定义包含的元素个数  
  17.             this.context = selector;            // 设置实例的属性,返回选择范围  
  18.             return this;                        // 返回当前实例  
  19.         }  
  20.         if(typeof selector === "string"){       // 如果选择符是字符串  
  21.             var e = context.getElementsByTagName(selector); // 获取指定名称的元素  
  22.             for(var i = 0; i<e.length; i++){ // 遍历元素集合,并把所有元素填入到当前实例数组中  
  23.                 this[i] = e[i];  
  24.             }  
  25.             this.length = e.length;             // 设置实例的 length 属性,即定义包含的元素个数  
  26.             this.context = context;             // 设置实例的属性,返回选择范围  
  27.             return this;                        // 返回当前实例  
  28.         } else {  
  29.             this.length = 0;                    // 否则,设置实例的 length 属性值为 0  
  30.             this.context = context;             // 设置实例的属性,返回选择范围  
  31.             return this;                        // 返回当前实例  
  32.         }  
  33.     },  
  34.     jquery: "1.3.2",                // 原型属性  
  35.     size: function(){               // 原型方法  
  36.         return this.length;  
  37.     }  
  38. };  
  39.   
  40. jQuery.fn.init.prototype = jQuery.fn; // 使用 jQuery 的原型对象覆盖 init 的原型对象  
  41. alert($("div").size());                 // 返回 3  
  42. </script>  
在上面示例中,$("div") 基本拥有了 jQuery 框架中 $("div") 语法的功能,使用它可以选取页面中指定范围的 div 元素。同时,调用 size() 方法可以返回 jQuery 对象集合的长度。

本文转载:CSDN博客