2.2.6 延续 -- 迭代器
在 jQuery 框架中,jQuery 对象是一个很奇怪的概念,具有多重身份,所以很多初学者一听说 jQuery 对象就感觉很是不解,误以为它是 John Resig 制造的新概念。我们可以对jQuery 对象进行如下分解。
第一,jQuery 对象是一个数据集合,它不是一个个体对象。因此,你无法直接使用 JavaScript 的方法来操作它。
第二,jQuery 对象实际上就是一个普通的对象,因为它是通过 new 运算符创建的一个新的实例对象。它可以继承原型方法或属性,同时也拥有 Object 类型的方法和属性。
第三,jQuery 对象包含数组特性,因为它赋值了数组元素,以数组结构存储返回的数据。我们可以以 JavaScript 的概念理解 jQuery 对象,例如下面的示例。
- <script type="text/javascript">
- var jquery = { // 定义对象直接量
- name: "jQuery", // 以属性方式存储信息
- value: "1.3.2"
- };
- jquery[0] = "jQuery"; // 以数组方式存储信息
- jquery[1] = "1.3.2";
- alert(jquery.name); // 返回 "jQuery"
- alert(jquery[0]); // 返回 "jQuery"
- </script>
第四,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 所示。
- <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; // 返回当前实例
- }
- },
- html: function(val){ // 模仿 jQuery 框架中的 html() 方法,为匹配的每一个DOM元素插入html代码
- jQuery.each(this, function(val){ // 调用 jQuery.each() 工具函数,为每一个 DOM 元素执行回调函数
- this.innerHTML = val;
- }, val);
- },
- jquery: "1.3.2", // 原型属性
- size: function(){ // 原型方法
- return this.length;
- }
- };
- jQuery.fn.init.prototype = jQuery.fn; // 使用 jQuery 的原型对象覆盖 init 的原型对象
- // 扩展 jQuery 工具函数
- jQuery.each = function(object, callback, args){
- for(var i=0; i<object.length; i++){
- callback.call(object[i], args);
- }
- return object;
- };
- $("div").html("测试代码");
- </script>
注意,在上面的代码中,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 对象的方法。
- <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 的原型对象
- // jQuery 功能扩展函数
- jQuery.extend = jQuery.fn.extend = function(obj){
- for(var prop in obj){
- this[prop] = obj[prop];
- }
- return this;
- };
- // 扩展 jQuery 对象方法
- jQuery.fn.extend({
- test: function(){
- alert("测试扩展功能");
- }
- });
- // 测试代码
- $("div").test();
- </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 框架的很多方法都包含大量的参数,且都是可选的,位置也没有固定要求,所以使用对象直接量是惟一的解决方法。
使用对象直接量作为参数传递的载体,这里就涉及参数处理问题。如何解析并提出参数?如何处理参数和默认值?我们可以通过下面的方法来实现。
- <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; // 返回当前实例
- }
- },
- setOptions: function(options){
- this.options = { // 方法的默认值,可以扩展
- StartColor: "#000",
- EndColor: "#DDC",
- Step: 20,
- Speed: 10
- };
- jQuery.extend(this.options, options || {}); // 如果传递参数,则覆盖原默认参数
- },
- jquery: "1.3.2", // 原型属性
- size: function(){ // 原型方法
- return this.length;
- }
- };
- jQuery.fn.init.prototype = jQuery.fn; // 使用 jQuery 的原型对象覆盖 init 的原型对象
- jQuery.extend = jQuery.fn.extend = function(destination, source){ // 重新定义 extend() 函数
- for (var property in source){
- destination[property] = source[property];
- }
- return destination;
- };
- </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 框架中是这样定义的。
- <script type="text/javascript">
- var jQuery = window.jQuery = window.$ = function(selector, context){
- return new jQuery.fn.init(selector, context);
- };
- </script>
(function(){
alert("观察我什么时候出现");
})();
这是一个典型的匿名函数基本形式。为什么要用到匿名函数呢?
这时就要进入正题了,如果希望自己的 jQuery 框架与其他任何代码完全隔离开来,也就是说如果你想把 jQuery 装在一个封闭空间中,不希望暴露内部信息,也不希望别的代码随意访问,匿名函数就是一种最好的封闭方式。此时我们只需要提供接口,就可以方便地与外界进行联系。例如,在下面的示例中分别把 f1 函数放在一个匿名函数中,而把 f2 函数放在全局作用域中。可以发现,全局作用域中的 f2 函数可以允许访问,而匿名函数中的 f1 函数是禁止外界访问的。
- <script type="text/javascript">
- (function(){
- function f1(){
- return "f1()";
- }
- })();
- function f2(){
- return "f2()";
- }
- alert(f2()); // 返回 "f2()"
- alert(f1()); // 抛出异常,禁止访问
- </script>
当然,$ 和 jQuery 名字并非是 jQuery 框架的专利,其他一些经典框架中也会用到 $ 名字,也许读者也会定义自己的变量 jQuery 。
在这之前我们需要让它与其他框架协同工作,这就带来一个问题,如果我们都使用 $ 作为简写形式就会发生冲突,为此 jQuery 提供了一个 noConflit() 方法,该方法能够实现禁止 jQuery 框架使用这两个名字。为了实现这样的目的,jQuery 在框架的最前面,先使用 _$ 和 _jQuery 临时变量寄存 $ 和 jQuery 这两个变量的内容,当需要禁用 jQuery 框架的名字时,可以使用一个临时变量 _$ 和 _jQuery 恢复 $ 和 jQuery 这两个变量的实际内容。实现代码如下。
- <script type="text/javascript">
- (function(){
- var
- window = this,
- undefined,
- _jQuery = window.jQuery, // 暂存 jQuery 变量内容
- _$ = window.$, // 暂存 $ 变量内容
- jQuery = window.jQuery = window.$ = function(selector, context){
- return new jQuery.fn.init(selector, context);
- },
- quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,
- isSimple = /^.[^:#\[\.,]*$/;
- jQuery.fn = jQuery.prototype = {
- 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; // 返回当前实例
- }
- },
- setOptions: function(options){
- this.options = { // 方法的默认值,可以扩展
- StartColor: "#000",
- EndColor: "#DDC",
- Step: 20,
- Speed: 10
- };
- jQuery.extend(this.options, options || {}); // 如果传递参数,则覆盖原默认参数
- },
- jquery: "1.3.2", // 原型属性
- size: function(){ // 原型方法
- return this.length;
- }
- };
- jQuery.fn.init.prototype = jQuery.fn; // 使用 jQuery 的原型对象覆盖 init 的原型对象
- jQuery.extend = jQuery.fn.extend = function(destination, source){ // 重新定义 extend() 函数
- for (var property in source){
- destination[property] = source[property];
- }
- return destination;
- };
- })();
- </script>