2.2 设计框架模型
为了帮助读者更容易理解jQuery框架,下面通过一个简单的模型来讲解jQuery框架的实现过程。
2.2.1 定义类型
在JavaScript中,构造函数就是函数,不要把它想得很复杂。在语法形态上,构造函数与普通的函数无异。一般使用小括号运算符来调用函数,执行一段代码。如果使用new运算符来调用函数,那么这个函数就变成构造函数了。
不同于其他的主流编程语言,JavaScript的构造函数并不是作为类的一个特定方法存在的,当任意一个普通函数用于创建一个类型时,它就被称作构造函数,也称为构造器。
构造函数有如下特点。
在函数内部,可以使用this引用实例对象。实例对象就是类型实例化后返回的对象。因此,借助this可以在构造函数体内为实例对象设置属性、添加方法。
构造函数一般不需要返回值,但允许使用返回语句return(不推荐)。如果返回值为非对象类型的值,则被忽略;如果返回对象类型的值,则将覆盖掉实例化对象,this就不再引用返回的对象。
构造函数必须使用new运算符调用。如果直接使用小括号运算符调用,这时它就不是构造函数,而是普通的函数。
在JavaScript中,可以把构造函数理解为一个类型,虽然不规范,但是很好用。这个类型是JavaScript面向对象编程的基础。
定义一个函数就相当于构建了一个类型,然后借助这个类型来实例化无数的对象。
【示例】下面代码定义一个jQuery类型,类名是jQuery。
var jQuery = function(){ //函数体 }
上面代码实际上定义了一个空的函数,函数体内没有包含任何代码,可以把它理解为一个空类型。
下面代码为jQuery扩展原型。
var jQuery = function(){} jQuery.prototype = { //扩展的原型对象 }
提示:这里读者需要理解JavaScript原型:
原型是JavaScript实现继承的基本机制,JavaScript为所有函数定义了一个原型属性—prototype,通过它可以访问类型的原型对象。原型对象是一个类型的公共对象,允许该类型的所有实例对象访问。
接着上面的示例代码,为jQuery的原型起个别名—fn。如果直接命名为fn,则表示它属于window对象,这样使用不安全。更安全的方法是为jQuery类型对象定义一个静态引用jQuery.fn,然后把jQuery的原型对象传递给这个属性Query.fn,实现代码如下:
jQuery.fn = jQuery.prototype = { //扩展的原型对象 }
在这里,jQuery.fn引用jQuery.prototype,因此要访问jQuery的原型对象,不仅可以使用jQuery.fn,还可以直接使用jQuery.prototype。
下面为jQuery类型也起个别名—$。
var $ = jQuery = function(){}
模仿jQuery框架,给jQuery原型添加两个成员,一个是原型属性version,另一个是原型方法size(),分别定义jQuery框架的版本号和jQuery对象的长度。
2.2.2 返回jQuery对象
在2.2.1节示例基础上,本节讲解如何调用原型成员:version属性和size()方法。
也许,读者可以使用下面的代码调用。
但是,jQuery并不是这样用的,它模仿类似下面的方法进行调用。
也就是说,jQuery没有使用new运算符调用jQuery构造函数,并实例化jQuery类型;而是使用小括号运算符调用jQuery()构造函数,然后在后面直接访问原型成员。
如何实现这样的操作呢?
【示例1】在jQuery构造函数中使用return语句返回一个jQuery实例。
执行下面的代码,会出现如图2.1所示的内存溢出错误。
$().version; $().size();
图2.1 提示内存溢出错误
这说明在构造函数内部实例化对象是允许的,因为这个操作导致死循环引用。
【示例2】下面尝试使用工厂模式进行设计:在jQuery()构造函数中返回jQuery的原型引用。
【示例3】示例2基本实现了$().size()这种形式的用法,但是在构造函数中直接返回原型对象,设计思路过于狭窄,无法实现框架内部的管理和扩展。下面模拟其他面向对象语言的设计模式:在类型内部定义一个初始化构造函数init(),当类型实例化后直接执行这个函数,然后返回jQuery的原型对象。
2.2.3 设计作用域
2.2.2节初步实现了最原始的想法:模拟jQuery的用法,让jQuery()返回jQuery类型的原型。实现方法:定义初始化函数init()并返回this,而this引用的是jQuery类型的原型jQuery.prototype。
在使用过程中也会发现一个问题:作用域混乱,给后期的扩展带来隐患。下面结合一个示例进行说明。
【示例1】定义jQuery原型中包含一个length属性,同时初始化函数init()内部也包含一个length属性和一个_size()方法。
运行示例,可以看到,init()函数内的this与外面的this均引用同一个对象:jQuery.prototype原型对象。因此,会出现init()函数内部的this.length覆盖掉外部的this.length。
简单概括:初始化函数init()的内、外作用域缺乏独立性,对于jQuery这样的框架来说,很可能造成消极影响。
翻看一下jQuery源码,可以看到jQuery框架通过下面的方式调用init()函数。
使用new运算符调用初始化函数init(),创建一个独立的实例对象,这样就分隔了init()函数内、外的作用域,确保内、外this引用不同。
【示例2】修改示例1中的jQuery(),使用return返回新创建的实例。
运行示例2,由于作用域被阻断,导致无法访问jQuery.fn对象的属性或方法。
2.2.4 跨域访问
下面来探索如何越过作用域的限制,实现跨域访问外部的jQuery.prototype。
分析jQuery框架源码,发现它是通过原型传递解决这个问题。实现方法:把jQuery.fn传递给jQuery. fn.init.prototype,用jQuery的原型对象覆盖掉init的原型对象,从而实现跨域访问。
【示例】下面代码具体演示了跨域访问的过程。
new jQuery.fn.init()用来创建一个新的实例对象,它拥有init类型的prototype原型对象。通过改变prototype指针,使其指向jQuery类的prototype,这样新实例实际上就继承了jQuery.fn原型对象的成员。
2.2.5 设计选择器
前面几节分步讲解了jQuery框架模型的最外层逻辑结构,下面再来探索jQuery内部的核心功能—选择器。
jQuery返回的是jQuery对象。实际上,jQuery是一个普通对象,拥有数组length,不继承数组的原型方法。
【示例】下面示例尝试为jQuery()函数传递一个参数,并让它返回一个jQuery对象。
翻看jQuery源码,可以看到jQuery()构造函数包含两个参数—selector和context,其中selector表示选择器,context表示匹配的下上文,即可选择的范围,它表示一个DOM元素。为了简化操作,本例假设选择器的类型仅为标签选择器。实现的代码如下:
在上面示例中,$("div")基本拥有了jQuery框架中$("div")选择器的功能,使用它可以选取页面中指定范围的div元素。同时,读取length属性可以返回jQuery对象的长度。
2.2.6 设计迭代器
2.2.5节探索了jQuery选择器的基本实现方法,下面讲解如何操作jQuery对象。
在jQuery框架中,jQuery对象是一个普通的JavaScript对象,但是它以索引数组的形式包含了一组数据,这组数据就是使用选择器匹配的所有DOM元素。
操作jQuery对象,实际上就是操作这些DOM元素,但是无法直接使用JavaScript方法来操作jQuery对象。只有逐一读取它包含的每个DOM元素才能够实现各种操作,如插入、删除、嵌套、赋值、读写属性等。
在实际使用jQuery的过程中,可以看到类似下面的jQuery用法。
$("div").html()
也就是说,可以直接在jQuery对象上调用html()方法来操作jQuery包含的所有DOM元素。那么这个功能是怎么实现的呢?
jQuery定义了一个工具函数each(),利用这个工具可以遍历jQuery对象中的所有DOM元素,并把操作jQuery对象的行为封装到一个回调函数中,然后通过在每个DOM元素上调用这个回调函数来实现逐一操作每个DOM元素。
实现代码如下:
在上面代码中,为jQuery对象绑定html()方法,然后利用jQuery()选择器获取页面中的所有div元素,调用html()方法,为所有匹配的元素插入HTML字符串。
注意:each()的当前作用对象是jQuery对象,故this指向当前jQuery对象;而在html()方法内部,由于是在指定DOM元素上执行操作,则this指向的是当前DOM元素,不再是jQuery对象。
最后,在页面中进行测试,代码如下:
<script> window.onload = function(){ $("div").html("<h1>你好</h1>"); } </script> <div></div> <div></div> <div></div>
预览效果如图2.2所示。
图2.2 操作jQuery对象
当然,上面示例所定义的each()函数和html()方法的功能比较有限。在jQuery框架中封装的each()函数功能就很强大,具体代码将在后面章节中详细讲解。
2.2.7 设计扩展
jQuery提供了良好的扩展接口,方便用户自定义jQuery方法。
根据设计习惯,如果为jQuery或者jQuery.prototype新增方法,直接通过点语法,或者在jQuery.prototype对象结构内增加即可。但是,如果分析jQuery源码,会发现它通过extend()函数来实现功能扩展。
【示例1】下面代码是jQuery框架通过extend()函数扩展的功能。
或者
这样做有什么好处呢?
方便用户快速扩展jQuery功能,但不会破坏jQuery框架的结构。如果直接在jQuery源码中添加方法,这样容易破坏jQuery框架的简洁性,也不方便后期代码维护。如果不需要某个插件,使用jQuery提供的扩展工具添加,只需要简单的删除即可,而不需要在jQuery源码中寻找要删除的代码段。
extend()函数的功能很简单,它只是把指定对象的方法复制给jQuery对象或者jQuery.prototype。
【示例2】为jQuery类型和jQuery对象定义了一个扩展函数extend(),设计把参数对象包含的所有属性复制给jQuery或者jQuery.prototype,这样就可以实现动态扩展jQuery的方法。
在上面示例中,先定义一个jQuery扩展函数extend(),然后为jQuery.fn原型对象调用extend()函数,为其添加一个jQuery方法html()。这样就可以设计出与2.2.6节相同的示例效果。
jQuery框架定义的extend()函数的功能要强大很多,它不仅能够完成基本的功能扩展,还可以实现对象合并等功能,详细代码将在后面章节中解析。
2.2.8 传递参数
很多jQuery方法,如果包含有参数,一般都要求传递参数对象,例如:
$.ajax({ type: "GET", url: "test.js", dataType: "script" });
使用对象直接量作为参数进行传递,方便参数管理。当方法或者函数的参数长度不固定时,使用对象直接量作为参数进行传递有如下优势。
参数个数不受限制。
参数顺序可以随意。
这体现了jQuery用法的灵活性。
如果ajax()函数的参数长度是固定的,则参数位置也固定,如$.ajax("GET", "test.js","script")。这种用法本身没有问题,但是很多jQuery方法包含大量的可选参数,参数位置没有必要限制,再使用传统方式来设计参数就比较麻烦。所以使用对象直接量作为参数进行传递,是最佳的解决方法。
【示例】使用对象直接量作为参数进行传递,这里就涉及参数处理问题,如何解析并提取参数,如何处理默认值问题,可以通过下面的方式来实现。
第1步,在前面示例基础上,重写编写jQuery.extend()工具函数。
在上面代码中重写了jQuery.extend()工具函数,让它实现两个功能:合并对象,为jQuery扩展插件。
为此,在工具函数中通过if条件语句检测参数对象arguments所包含的参数个数,以及参数类型,来决定是合并对象,还是扩展插件。
如果用户给了两个参数,且都为对象,则把第2个对象合并到第1个对象中,并返回第1个对象;如果用户给了一个参数,则继续沿用前面的设计方法,把参数对象复制到jQuery原型对象上实现插件扩展。
第2步,利用jQuery.extend()工具函数为jQuery扩展一个插件fontStyle(),使用这个插件可以定义网页字体样式。
在上面的插件函数fontStyle()中,首先,定义一个默认配置对象defaults,初始化字体样式:字体颜色为黑色,字体背景色为白色,字体大小为14像素,字体样式为正常。
其次,使用jQuery.extend()工具函数,把用户传递的参数对象obj合并到默认配置参数对象defaults,返回并覆盖掉defaults对象。为了避免用户没有传递参数,可以使用“obj || {}”检测用户是否传递参数对象,如果没有,则使用空对象参与合并操作。
最后,使用迭代函数jQuery.each()逐个访问jQuery对象中包含的DOM元素,然后分别为它设置字体样式。
第3步,在页面中调用jQuery查找所有段落文本p,然后调用fontStyle方法设置字体颜色为白色,字体背景色为黑色,字体大小为24像素,字体样式保持默认值。
第4步,在<body>内设计两段文本,最后在浏览器中查看效果,如图2.3所示。
<p>少年不识愁滋味,爱上层楼。爱上层楼,为赋新词强说愁。</p> <p>而今识尽愁滋味,欲说还休。欲说还休,却道天凉好个秋。</p>
图2.3 实现jQuery扩展的参数传递
在jQuery框架中,extend()函数功能很强大,它既能够作为jQuery的扩展方法,也能够处理参数对象,并覆盖默认值,在后面章节中将详细分析它的源码。
2.2.9 设计独立空间
在页面中引入多个JavaScript框架,或者编写了大量JavaScript代码时,用户很难确保这些代码不发生冲突。任何人都无法确保自己很熟悉每个框架的源码,难免会出现名字冲突,或者功能覆盖现象。为了解决这个问题,必须把jQuery封装在一个孤立的环境中,避免与其他代码相互干扰。
解决这个问题,一般使用JavaScript闭包体来实现。
首先,读者要知道,JavaScript有且仅有两个作用域:全局作用域和函数作用域。函数作用域是局部作用域,对外是不可见的,外部代码无权访问函数体内的代码。
调用函数时,JavaScript会自动为其生成一个上下文环境,这个环境是临时的,函数调用完毕后,这个上下文环境会自动被注销。
提示:如果每次调用函数后,上下文环境都被保留,那么内存溢出就不可避免,因为在页面中有大量的函数调用操作,一个浏览器可能会开启很多页面,每个上下文环境都要占据一定的内存空间,只进不出,内存肯定会被宕机。所以,调用完毕后,就没有必要继续保留这个上下文环境。
【示例1】设计一个匿名函数,然后自调用,瞬间产生一个临时的上下文环境。
(function(){ //函数体 })()
其次,如果在匿名函数中引用了外部变量,那么只要这个引用一直存在,则生成的上下文环境就一直存在,这样就产生了闭包体。所以,闭包体是一直存在的,除非手动销毁外部的引用。
闭包体的存在也改变了函数调用后的运行逻辑,这时调用对象一直存在,并一直保存着函数内各种局部变量的信息。
【示例2】把外部window对象传递给匿名函数,则自调用后,函数体内的私有变量window与外部变量window就一直保持着引用关系,这个上下文环境也就一直存在。
(function(window){ //函数体 })(window)
最后,对于函数作用域来说,也必须通过这种方式保持与外界的联系,否则外界无法访问内部的信息,则定义的闭包体也就没有存在的价值了。
如果希望jQuery框架与其他代码完全隔离,闭包体是一种最佳的方式。
【示例3】把2.2.8节设计的jQuery框架模型放入匿名函数中,然后自调用,并传入window对象。
倒数第二行代码“window.jQuery = window.$ = jQuery;”的主要作用是:把闭包体内的私有变量jQuery传递给参数对象window的jQuery属性,而参数对象window引用外部传入的window变量,window变量引用全局对象window。所以,在全局作用域中就可以通过jQuery变量来访问闭包体内的jQuery框架,通过这种方式向外界暴露自己,允许外界使用jQuery框架。但是,外界只能访问jQuery,不能访问闭包体内其他私有变量。
至此,jQuery框架的设计模型就基本完成了,后面的工作就是根据需要使用extend()函数扩展jQuery功能。例如,在闭包体外直接引用jQuery.fn.extend()函数为jQuery扩展fontStyle插件。
使用下面代码就可以在页面中使用这个插件了。
上面代码与2.2.8节相同,这里不再赘述,本例完整代码请参考本节示例源代码。