2.2.6 延续 -- 迭代器

在 jQuery 框架中,jQuery 对象是一个很奇怪的概念,具有多重身份,所以很多初学者一听说 jQuery 对象就感觉很是不解,误以为它是 John Resig 制造的新概念。我们可以对jQuery 对象进行如下分解。

第一,jQuery 对象是一个数据集合,它不是一个个体对象。因此,你无法直接使用 JavaScript 的方法来操作它。

第二,jQuery 对象实际上就是一个普通的对象,因为它是通过 new 运算符创建的一个新的实例对象。它可以继承原型方法或属性,同时也拥有 Object 类型的方法和属性。

第三,jQuery 对象包含数组特性,因为它赋值了数组元素,以数组结构存储返回的数据。我们可以以 JavaScript 的概念理解 jQuery 对象,例如下面的示例。

[html] view plaincopy
  1. <script type="text/javascript">  
  2.     var jquery = {          // 定义对象直接量  
  3.         name: "jQuery",     // 以属性方式存储信息  
  4.         value: "1.3.2"  
  5.     };  
  6.     jquery[0] = "jQuery";   // 以数组方式存储信息  
  7.     jquery[1] = "1.3.2";      
  8.     alert(jquery.name);     // 返回 "jQuery"  
  9.     alert(jquery[0]);       // 返回 "jQuery"  
  10. </script>  
上面的 jQuery 对象就是一个典型的 jQuery 对象,jQuery 对象的结构就是按这种形式设计的。可以说,jQuery 对象就是对象和数组的混合体,但是它不拥有数组的方法,因为它的数组结构是人为附加的,也就是说它不是 Array 类型数据,而是 Object 类型数据。

第四,jQuery 对象包含的数据都是 DOM 元素,是通过数组形式存储的,即通过 jQuery[n] 形式获取。同时 jQuery 对象又定义了几个模仿 Array 基本特性的属性,如 length 等。

所以,jQuery 对象是不允许直接操作的,只有分别读取它包含的每一个 DOM 元素,才能实现各种操作,如插入、删除、嵌套、赋值和读写 DOM 元素属性等。

那么如何实现直接操作 jQuery 对象中的 DOM 元素呢?

在实际应用中,我们可以看到类似下面的 jQuery 用法。

$("div").html()

也就是直接在 jQuery 对象上调用 html(),并实现操作 jQuery 包含的所有 DOM 元素。那么这个功能是怎么实现的呢?

jQuery 定义了一个工具函数 each(),利用这个工具可以遍历 jQuery 对象中所有的 DOM 元素,并把需要操作的内容封装到一个回调函数中,然后通过在每个 DOM 元素上调用这个回调函数即可。实现代码如下所示,演示效果如图 2.2 所示。

[html] view plaincopy
  1. <script type="text/javascript">  
  2. var $ = jQuery = function(selector, context){           // 定义类        
  3.     return new jQuery.fn.init(selector, context);       // 返回选择器的实例  
  4. };  
  5.   
  6. jQuery.fn = jQuery.prototype = {                // jQuery 类的原型对象  
  7.     init: function(selector, context){          // 定义选择器构造器  
  8.         selector = selector || document;        // 设置默认值为 document  
  9.         context = context || document;          // 设置默认值为 document  
  10.           
  11.         if(selector.nodeType){                  // 如果选择符为节点对象  
  12.             this[0] = selector;                 // 把参数节点传递给实例对象的数组  
  13.             this.length = 1;                    // 并设置实例对象的 length 属性,定义包含的元素个数  
  14.             this.context = selector;            // 设置实例的属性,返回选择范围  
  15.             return this;                        // 返回当前实例  
  16.         }  
  17.         if(typeof selector === "string"){       // 如果选择符是字符串  
  18.             var e = context.getElementsByTagName(selector); // 获取指定名称的元素  
  19.             for(var i = 0; i<e.length; i++){ // 遍历元素集合,并把所有元素填入到当前实例数组中  
  20.                 this[i] = e[i];  
  21.             }  
  22.             this.length = e.length;             // 设置实例的 length 属性,即定义包含的元素个数  
  23.             this.context = context;             // 设置实例的属性,返回选择范围  
  24.             return this;                        // 返回当前实例  
  25.         } else {  
  26.             this.length = 0;                    // 否则,设置实例的 length 属性值为 0  
  27.             this.context = context;             // 设置实例的属性,返回选择范围  
  28.             return this;                        // 返回当前实例  
  29.         }  
  30.     },  
  31.     html: function(val){        // 模仿 jQuery 框架中的 html() 方法,为匹配的每一个DOM元素插入html代码  
  32.         jQuery.each(this, function(val){    // 调用 jQuery.each() 工具函数,为每一个 DOM 元素执行回调函数  
  33.             this.innerHTML = val;  
  34.         }, val);  
  35.     },  
  36.     jquery: "1.3.2",                // 原型属性  
  37.     size: function(){               // 原型方法  
  38.         return this.length;  
  39.     }  
  40. };  
  41.   
  42. jQuery.fn.init.prototype = jQuery.fn; // 使用 jQuery 的原型对象覆盖 init 的原型对象  
  43. // 扩展 jQuery 工具函数  
  44. jQuery.each = function(object, callback, args){  
  45.     for(var i=0; i<object.length; i++){  
  46.         callback.call(object[i], args);  
  47.     }  
  48.     return object;  
  49. };  
  50.   
  51. $("div").html("测试代码");  
  52. </script>  
在上面的示例中,通过先为自己的 jQuery 对象绑定 html() 方法,然后利用 jQuery() 选择器获取页面中所有的 div 元素,再调用 html() 方法,为所有匹配的元素插入 HTML 源码。

注意,在上面的代码中,each() 函数的当前作用对象是 jQuery 对象,故 this 指向当前 jQuery 对象,即 this 表示一个集合对象;而在 html() 方法中,由于 each() 函数是在指定 DOM 元素上执行的,所以该函数内的 this 指针指向的是当前 DOM 元素对象,即 this 表示一个元素。


2.2.7 延续 -- 功能扩展

根据一般设计习惯,如果要为 jQuery 或者 jQuery.prototype 添加函数或方法,可以直接通过点语法实现,或者在 jQuery.prototype 对象结构中增加一个属性即可。但是,如果分析 jQuery 框架的源代码,你会发现它是通过 extend() 函数来实现功能扩展的。例如,下面两段代码都是 jQuery 框架通过 extend() 函数来扩展功能的。

jQuery.extend({          // 扩展工具函数

noConflict: function(deep){},

isFunction: function(obj){},

isArray: function(obj){},

isXMLDoc: function(elem){},

globalEval: function(data){}

});

或者

jQuery.fn.extend({  // 扩展 jQuery 对象方法

show: function(speed, callback){},

hide: function(speed, callback){},

toggle: function(fn, fn2){},

fadeTo: function(speed, to, callback){},

animate: function(prop, speed, easing, callback){},

stop: function(clearQueue, gotoEnd){}

});

这样做的好处是什么呢?

extend() 函数能够方便用户快速扩展 jQuery 框架的功能,但是不会破坏 jQuery 框架的原型结构,从而避免后期人工手动添加工具函数或者方法破坏 jQuery 框架的单纯性,同时也方便管理。如果不需要某个插件,只需要简单地删除即可,而不需要在 jQuery 框架源代码中去筛选和删除。

extend() 函数的功能实现起来也很简单,它只是把指定对象的方法复制给 jQuery 对象或者 jQuery.prototype 对象。例如,在下面的示例中,我们为 jQuery 类和原型定义了一个扩展功能的函数 extend() ,该函数的功能很简单,它能够把指定参数对象包含的所有属性复制给 jQuery 或者 jQuery.prototype 对象,这样就可以在应用中随时调用它,并动态扩展 jQuery 对象的方法。

[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. // jQuery 功能扩展函数  
  42. jQuery.extend = jQuery.fn.extend = function(obj){  
  43.     for(var prop in obj){  
  44.         this[prop] = obj[prop];  
  45.     }  
  46.     return this;  
  47. };  
  48. // 扩展 jQuery 对象方法  
  49. jQuery.fn.extend({  
  50.     test: function(){  
  51.         alert("测试扩展功能");  
  52.     }  
  53. });  
  54. // 测试代码  
  55. $("div").test();  
  56. </script>  

在上面的示例中,先定义了一个功能扩展函数 extend(),然后为 jQuery.fn 原型对象调用 extend() 函数,为其添加一个测试方法 test()。这样就可以在实践中应用,如 $("div").test() 。

jQuery 框架定义的 extend() 函数的功能要强大很多,它不仅能够完成基本的功能扩展,还可以实现对象合并等功能。

2.2.8 延续 -- 参数处理

在很多时候,你会发现 jQuery 的方法都要求传递的参数为对象结构,例如:

$.ajax({

type: "GET",

url: "test.js",

dataType: "script"

});

使用对象直接量作为参数进行传递,方便参数管理。当方法或者函数的参数长度不固定时,使用对象直接量作为参数存在很多优势。例如,对于下面的用法,ajax()函数就需要进行更加复杂的参数排查和过滤。

$.ajax("GET", "test.js", "script");

如果 ajax() 函数的参数长度是固定的,且是必须的,那么通过这种方式进行传递也就无所谓了,但是如果参数的个数和排序是动态的,那么使用 $.ajax("GET", "test.js", "script"); 这种方法是无法处理的。而 jQuery 框架的很多方法都包含大量的参数,且都是可选的,位置也没有固定要求,所以使用对象直接量是惟一的解决方法。

使用对象直接量作为参数传递的载体,这里就涉及参数处理问题。如何解析并提出参数?如何处理参数和默认值?我们可以通过下面的方法来实现。

[html] view plaincopy
  1. <script type="text/javascript">  
  2. var $ = jQuery = function(selector, context){           // 定义类        
  3.     return new jQuery.fn.init(selector, context);       // 返回选择器的实例  
  4. };  
  5.   
  6. jQuery.fn = jQuery.prototype = {                // jQuery 类的原型对象  
  7.     init: function(selector, context){          // 定义选择器构造器  
  8.         selector = selector || document;        // 设置默认值为 document  
  9.         context = context || document;          // 设置默认值为 document  
  10.           
  11.         if(selector.nodeType){                  // 如果选择符为节点对象  
  12.             this[0] = selector;                 // 把参数节点传递给实例对象的数组  
  13.             this.length = 1;                    // 并设置实例对象的 length 属性,定义包含的元素个数  
  14.             this.context = selector;            // 设置实例的属性,返回选择范围  
  15.             return this;                        // 返回当前实例  
  16.         }  
  17.         if(typeof selector === "string"){       // 如果选择符是字符串  
  18.             var e = context.getElementsByTagName(selector); // 获取指定名称的元素  
  19.             for(var i = 0; i<e.length; i++){ // 遍历元素集合,并把所有元素填入到当前实例数组中  
  20.                 this[i] = e[i];  
  21.             }  
  22.             this.length = e.length;             // 设置实例的 length 属性,即定义包含的元素个数  
  23.             this.context = context;             // 设置实例的属性,返回选择范围  
  24.             return this;                        // 返回当前实例  
  25.         } else {  
  26.             this.length = 0;                    // 否则,设置实例的 length 属性值为 0  
  27.             this.context = context;             // 设置实例的属性,返回选择范围  
  28.             return this;                        // 返回当前实例  
  29.         }  
  30.     },  
  31.     setOptions: function(options){  
  32.         this.options = {    // 方法的默认值,可以扩展  
  33.             StartColor: "#000",  
  34.             EndColor: "#DDC",  
  35.             Step: 20,  
  36.             Speed: 10     
  37.         };  
  38.         jQuery.extend(this.options, options || {}); // 如果传递参数,则覆盖原默认参数    
  39.     },  
  40.     jquery: "1.3.2",                // 原型属性  
  41.     size: function(){               // 原型方法  
  42.         return this.length;  
  43.     }  
  44. };  
  45.   
  46. jQuery.fn.init.prototype = jQuery.fn; // 使用 jQuery 的原型对象覆盖 init 的原型对象  
  47.   
  48. jQuery.extend = jQuery.fn.extend = function(destination, source){   // 重新定义 extend() 函数  
  49.     for (var property in source){  
  50.         destination[property] = source[property];  
  51.     }  
  52.     return destination;  
  53. };  
  54. </script>  

在上面的示例中,定义了一个原型方法 setOptions(),该方法能够对传递的参数对象进行处理,并覆盖默认值。这种用法在本书插件部分还将进行讲解。

在 jQuery 框架中, extend() 函数包含了所有功能,它既能够为当前对象扩展方法,也能够处理参数对象,并覆盖默认值。

2.2.9 涅槃 -- 名字空间

现在,我们终于模拟出了 jQuery 框架的雏形,虽然它还比较稚嫩,经不起风雨,但至少能够保证读者理解 jQuery 框架构成的初期状态。不过对于一个成熟的框架来说,需要设计者考虑的问题还是很多的,其中最核心的问题就是名字空间冲突问题。

当一个页面中存在多个框架,或者自己写了很多 JavaScript 代码,我们是很难确保这些代码不发生冲突的,因为任何人都无法确保自己非常熟悉 jQuery 框架中的每一行代码,所以难免会出现名字冲突,或者功能覆盖现象。为了解决这个问题,我们必须把 jQuery 封装在一个孤立的环境中,避免其他代码的干扰。

在详细讲解名字空间之前,我们先来温习两个 JavaScript 概念。首先,请看下面的代码。

var jQuery = function(){};

jQuery = function(){};

上面所示的代码是两种不同的写法,且都是合法的,但是它们的语义完全不同。第一行代码声明了一个变量,而第二行代码定义了 Window 对象的一个属性,也就是说它等同于下面的语句。

window.jQuery = function();

在全局作用域中,变量和 Window 对象的属性是可以相等的,也可以是互通的,但是当在其他环境中 (如局部作用域中),则它们是不相等的,也是无法互通的。

因此如果希望 jQuery 具有类似 $.method(); 调用方式的能力,就需要将 jQuery 设置为 Window 对象的一个属性,所以你就会看到 jQuery 框架中是这样定义的。

[html] view plaincopy
  1. <script type="text/javascript">  
  2.     var jQuery = window.jQuery = window.$ = function(selector, context){  
  3.         return new jQuery.fn.init(selector, context);  
  4.     };  
  5. </script>  
你可能看到过下面的函数用法。

(function(){

alert("观察我什么时候出现");

})();

这是一个典型的匿名函数基本形式。为什么要用到匿名函数呢?

这时就要进入正题了,如果希望自己的 jQuery 框架与其他任何代码完全隔离开来,也就是说如果你想把 jQuery 装在一个封闭空间中,不希望暴露内部信息,也不希望别的代码随意访问,匿名函数就是一种最好的封闭方式。此时我们只需要提供接口,就可以方便地与外界进行联系。例如,在下面的示例中分别把 f1 函数放在一个匿名函数中,而把 f2 函数放在全局作用域中。可以发现,全局作用域中的 f2 函数可以允许访问,而匿名函数中的 f1 函数是禁止外界访问的。

[html] view plaincopy
  1. <script type="text/javascript">  
  2.     (function(){  
  3.         function f1(){  
  4.             return "f1()";  
  5.         }  
  6.     })();  
  7.       
  8.     function f2(){  
  9.         return "f2()";  
  10.     }  
  11.       
  12.     alert(f2());    // 返回 "f2()"  
  13.     alert(f1());    // 抛出异常,禁止访问  
  14. </script>  
实际上,上面的匿名函数就是所谓的闭包,闭包是 JavaScript 函数中一个最核心的概念。

当然,$ 和 jQuery 名字并非是 jQuery 框架的专利,其他一些经典框架中也会用到 $ 名字,也许读者也会定义自己的变量 jQuery 。

在这之前我们需要让它与其他框架协同工作,这就带来一个问题,如果我们都使用 $ 作为简写形式就会发生冲突,为此 jQuery 提供了一个 noConflit() 方法,该方法能够实现禁止 jQuery 框架使用这两个名字。为了实现这样的目的,jQuery 在框架的最前面,先使用 _$ 和 _jQuery 临时变量寄存 $ 和 jQuery 这两个变量的内容,当需要禁用 jQuery 框架的名字时,可以使用一个临时变量 _$ 和 _jQuery 恢复 $ 和 jQuery 这两个变量的实际内容。实现代码如下。

[html] view plaincopy
  1. <script type="text/javascript">  
  2. (function(){      
  3. var   
  4.     window = this,  
  5.     undefined,  
  6.     _jQuery = window.jQuery,    // 暂存 jQuery 变量内容  
  7.     _$ = window.$,              // 暂存 $ 变量内容  
  8.     jQuery = window.jQuery = window.$ = function(selector, context){  
  9.         return new jQuery.fn.init(selector, context);  
  10.     },  
  11.     quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,  
  12.     isSimple = /^.[^:#\[\.,]*$/;  
  13.       
  14. jQuery.fn = jQuery.prototype = {  
  15.     init: function(selector, context){          // 定义选择器构造器  
  16.         selector = selector || document;        // 设置默认值为 document  
  17.         context = context || document;          // 设置默认值为 document  
  18.           
  19.         if(selector.nodeType){                  // 如果选择符为节点对象  
  20.             this[0] = selector;                 // 把参数节点传递给实例对象的数组  
  21.             this.length = 1;                    // 并设置实例对象的 length 属性,定义包含的元素个数  
  22.             this.context = selector;            // 设置实例的属性,返回选择范围  
  23.             return this;                        // 返回当前实例  
  24.         }  
  25.         if(typeof selector === "string"){       // 如果选择符是字符串  
  26.             var e = context.getElementsByTagName(selector); // 获取指定名称的元素  
  27.             for(var i = 0; i<e.length; i++){ // 遍历元素集合,并把所有元素填入到当前实例数组中  
  28.                 this[i] = e[i];  
  29.             }  
  30.             this.length = e.length;             // 设置实例的 length 属性,即定义包含的元素个数  
  31.             this.context = context;             // 设置实例的属性,返回选择范围  
  32.             return this;                        // 返回当前实例  
  33.         } else {  
  34.             this.length = 0;                    // 否则,设置实例的 length 属性值为 0  
  35.             this.context = context;             // 设置实例的属性,返回选择范围  
  36.             return this;                        // 返回当前实例  
  37.         }  
  38.     },  
  39.     setOptions: function(options){  
  40.         this.options = {    // 方法的默认值,可以扩展  
  41.             StartColor: "#000",  
  42.             EndColor: "#DDC",  
  43.             Step: 20,  
  44.             Speed: 10     
  45.         };  
  46.         jQuery.extend(this.options, options || {}); // 如果传递参数,则覆盖原默认参数    
  47.     },  
  48.     jquery: "1.3.2",                // 原型属性  
  49.     size: function(){               // 原型方法  
  50.         return this.length;  
  51.     }  
  52. };  
  53.   
  54. jQuery.fn.init.prototype = jQuery.fn; // 使用 jQuery 的原型对象覆盖 init 的原型对象  
  55.   
  56. jQuery.extend = jQuery.fn.extend = function(destination, source){   // 重新定义 extend() 函数  
  57.     for (var property in source){  
  58.         destination[property] = source[property];  
  59.     }  
  60.     return destination;  
  61. };  
  62. })();  
  63. </script>  
至此,jQuery 框架的设计模式就初见端倪了,后面的工作就是根据应用需要或者功能需要,使用 extend() 函数不断扩展 jQuery 的工具函数和 jQuery 对象的方法。

本文转载:CSDN博客