![jQuery从入门到精通(微课精编版)](https://wfqqreader-1252317822.image.myqcloud.com/cover/735/26542735/b_26542735.jpg)
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对象的长度。
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P29_40154.jpg?sign=1739344143-FsqvfjHYt3iTuMqs5LYsn9kZHlalx1x2-0-3db53cb4f06864170ce674f68f02e98d)
2.2.2 返回jQuery对象
在2.2.1节示例基础上,本节讲解如何调用原型成员:version属性和size()方法。
也许,读者可以使用下面的代码调用。
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P29_9408.jpg?sign=1739344143-R9vDtpg02Xqr5gut4IxScxDQOmEQWQtv-0-5c18f79e2971948b3845254fa589d04a)
但是,jQuery并不是这样用的,它模仿类似下面的方法进行调用。
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P29_40156.jpg?sign=1739344143-2ZeYdVmRV0LE6b0fv6DdecwnTk2IPjip-0-64be54673c2908341f9e24cf4d80e332)
也就是说,jQuery没有使用new运算符调用jQuery构造函数,并实例化jQuery类型;而是使用小括号运算符调用jQuery()构造函数,然后在后面直接访问原型成员。
如何实现这样的操作呢?
【示例1】在jQuery构造函数中使用return语句返回一个jQuery实例。
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P30_40229.jpg?sign=1739344143-mybXfhAclpvClOvcoJ288tJJgWz6KC4Z-0-4762955269b402b7c066d1014b3b097f)
执行下面的代码,会出现如图2.1所示的内存溢出错误。
$().version; $().size();
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P30_40223.jpg?sign=1739344143-jvnaN1wZuvWijL7IHJC2SDyflrBcFkL7-0-d8cccc6efb15c5dc8bf531ed6962c8a9)
图2.1 提示内存溢出错误
这说明在构造函数内部实例化对象是允许的,因为这个操作导致死循环引用。
【示例2】下面尝试使用工厂模式进行设计:在jQuery()构造函数中返回jQuery的原型引用。
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P30_40233.jpg?sign=1739344143-dbVAy7sDw9OIpbu7hGxUg9V2Psb65bjh-0-149d39fec010000d4f67bca976d473d6)
【示例3】示例2基本实现了$().size()这种形式的用法,但是在构造函数中直接返回原型对象,设计思路过于狭窄,无法实现框架内部的管理和扩展。下面模拟其他面向对象语言的设计模式:在类型内部定义一个初始化构造函数init(),当类型实例化后直接执行这个函数,然后返回jQuery的原型对象。
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P30_40235.jpg?sign=1739344143-ewUkSBNR7VbwueyDaLyxb6e5JR2vlsHr-0-90144050e0d5325d54eef6dc39cc5873)
2.2.3 设计作用域
2.2.2节初步实现了最原始的想法:模拟jQuery的用法,让jQuery()返回jQuery类型的原型。实现方法:定义初始化函数init()并返回this,而this引用的是jQuery类型的原型jQuery.prototype。
在使用过程中也会发现一个问题:作用域混乱,给后期的扩展带来隐患。下面结合一个示例进行说明。
【示例1】定义jQuery原型中包含一个length属性,同时初始化函数init()内部也包含一个length属性和一个_size()方法。
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P31_40237.jpg?sign=1739344143-jWZA5iSqtjU5V5pG8RbuuMDFXumVgJK5-0-7a67a44b369865f302d736b4bec8de9b)
运行示例,可以看到,init()函数内的this与外面的this均引用同一个对象:jQuery.prototype原型对象。因此,会出现init()函数内部的this.length覆盖掉外部的this.length。
简单概括:初始化函数init()的内、外作用域缺乏独立性,对于jQuery这样的框架来说,很可能造成消极影响。
翻看一下jQuery源码,可以看到jQuery框架通过下面的方式调用init()函数。
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P32_40239.jpg?sign=1739344143-mSTiZ2vxGcZG6H0Zi9l9ArjtMey7WA77-0-723bf1625d33cc1d6bb5ed90b98f24d2)
使用new运算符调用初始化函数init(),创建一个独立的实例对象,这样就分隔了init()函数内、外的作用域,确保内、外this引用不同。
【示例2】修改示例1中的jQuery(),使用return返回新创建的实例。
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P32_40241.jpg?sign=1739344143-i2BUA3PC74dRVn2Olpp03fOnLJjV7KNS-0-2286eed716b85d3448daf1256873aa95)
运行示例2,由于作用域被阻断,导致无法访问jQuery.fn对象的属性或方法。
2.2.4 跨域访问
下面来探索如何越过作用域的限制,实现跨域访问外部的jQuery.prototype。
分析jQuery框架源码,发现它是通过原型传递解决这个问题。实现方法:把jQuery.fn传递给jQuery. fn.init.prototype,用jQuery的原型对象覆盖掉init的原型对象,从而实现跨域访问。
【示例】下面代码具体演示了跨域访问的过程。
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P32_40243.jpg?sign=1739344143-K3sVkDtAONxO7bhxmSMgPeoFOxVqMwze-0-e518a7b226920d5c52c9f17cef3ddd54)
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元素。为了简化操作,本例假设选择器的类型仅为标签选择器。实现的代码如下:
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P33_40245.jpg?sign=1739344143-IvuevBcxTOeFuDRQnHddicHzTsz5e9Dp-0-98ef93c231959c1585fe8ee4b3c18a17)
在上面示例中,$("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元素。
实现代码如下:
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P34_9792.jpg?sign=1739344143-MEFIhndjBrW9ZQeVZNK3FtqFgUuAUaOa-0-38aa908f4e307eae31bd0a8e8a22d544)
在上面代码中,为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所示。
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P36_9943.jpg?sign=1739344143-QKLZWt5Vc5JSFqesdDHv7LwgtZxyn2QQ-0-0b0af6d4770bf3a6a89aee5bb8f012d8)
图2.2 操作jQuery对象
当然,上面示例所定义的each()函数和html()方法的功能比较有限。在jQuery框架中封装的each()函数功能就很强大,具体代码将在后面章节中详细讲解。
2.2.7 设计扩展
jQuery提供了良好的扩展接口,方便用户自定义jQuery方法。
根据设计习惯,如果为jQuery或者jQuery.prototype新增方法,直接通过点语法,或者在jQuery.prototype对象结构内增加即可。但是,如果分析jQuery源码,会发现它通过extend()函数来实现功能扩展。
【示例1】下面代码是jQuery框架通过extend()函数扩展的功能。
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P36_40254.jpg?sign=1739344143-NoFIjuxf1SS1n2v5fK1QwrTnctb4J4uc-0-a4f514434daa9b2818534dbb15d163bd)
或者
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P36_40255.jpg?sign=1739344143-CCu8QqosRHdWlb9wry5bJzNe7ftB0Jrn-0-49ac5084975c86a2228f91f475dc7beb)
这样做有什么好处呢?
方便用户快速扩展jQuery功能,但不会破坏jQuery框架的结构。如果直接在jQuery源码中添加方法,这样容易破坏jQuery框架的简洁性,也不方便后期代码维护。如果不需要某个插件,使用jQuery提供的扩展工具添加,只需要简单的删除即可,而不需要在jQuery源码中寻找要删除的代码段。
extend()函数的功能很简单,它只是把指定对象的方法复制给jQuery对象或者jQuery.prototype。
【示例2】为jQuery类型和jQuery对象定义了一个扩展函数extend(),设计把参数对象包含的所有属性复制给jQuery或者jQuery.prototype,这样就可以实现动态扩展jQuery的方法。
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P37_10061.jpg?sign=1739344143-BQM6YudkxasLMtUbRjfqMMV8OLZSAv9L-0-b799899901871d098b488f34462e67d2)
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P38_40256.jpg?sign=1739344143-36gSz3qTgFrfr8PcIk96ok10zpcKpxiC-0-1a3ca81e329faba6fb93783d90a87431)
在上面示例中,先定义一个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()工具函数。
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P38_10128.jpg?sign=1739344143-p2OaxLZVzNakFYTK1QpXOb9yOfeOuXYR-0-d16dfdcae44f81c2d86142ebd4eba224)
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P39_40262.jpg?sign=1739344143-7sMMq6bNLeqsMPPJRO51DLZZQC0tI3Sl-0-2cdd97e1d228a7f729a5b28cb921bec7)
在上面代码中重写了jQuery.extend()工具函数,让它实现两个功能:合并对象,为jQuery扩展插件。
为此,在工具函数中通过if条件语句检测参数对象arguments所包含的参数个数,以及参数类型,来决定是合并对象,还是扩展插件。
如果用户给了两个参数,且都为对象,则把第2个对象合并到第1个对象中,并返回第1个对象;如果用户给了一个参数,则继续沿用前面的设计方法,把参数对象复制到jQuery原型对象上实现插件扩展。
第2步,利用jQuery.extend()工具函数为jQuery扩展一个插件fontStyle(),使用这个插件可以定义网页字体样式。
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P39_10221.jpg?sign=1739344143-9TZ7wCa5gX6NhRM49OJcZZmQEDq1y71Q-0-17e54f7475cbbaaa41e3c7defd0ea3ca)
在上面的插件函数fontStyle()中,首先,定义一个默认配置对象defaults,初始化字体样式:字体颜色为黑色,字体背景色为白色,字体大小为14像素,字体样式为正常。
其次,使用jQuery.extend()工具函数,把用户传递的参数对象obj合并到默认配置参数对象defaults,返回并覆盖掉defaults对象。为了避免用户没有传递参数,可以使用“obj || {}”检测用户是否传递参数对象,如果没有,则使用空对象参与合并操作。
最后,使用迭代函数jQuery.each()逐个访问jQuery对象中包含的DOM元素,然后分别为它设置字体样式。
第3步,在页面中调用jQuery查找所有段落文本p,然后调用fontStyle方法设置字体颜色为白色,字体背景色为黑色,字体大小为24像素,字体样式保持默认值。
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P40_40265.jpg?sign=1739344143-j1fJE3vJypqMwZEfdGFAPSM6WghItAgT-0-d95f532b740ba5e43276de53b81eb79f)
第4步,在<body>内设计两段文本,最后在浏览器中查看效果,如图2.3所示。
<p>少年不识愁滋味,爱上层楼。爱上层楼,为赋新词强说愁。</p> <p>而今识尽愁滋味,欲说还休。欲说还休,却道天凉好个秋。</p>
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P40_10289.jpg?sign=1739344143-xmMoQq2i1Au5guiicgQSM21lmw4tdkgW-0-def1bf2e945824d1f4e96781c6429eb2)
图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对象。
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P41_40272.jpg?sign=1739344143-qAllS73nK5ZvnwP1u762Uzqd2qZPrq42-0-8c02fd33dcea2eaba76482e73caf7741)
倒数第二行代码“window.jQuery = window.$ = jQuery;”的主要作用是:把闭包体内的私有变量jQuery传递给参数对象window的jQuery属性,而参数对象window引用外部传入的window变量,window变量引用全局对象window。所以,在全局作用域中就可以通过jQuery变量来访问闭包体内的jQuery框架,通过这种方式向外界暴露自己,允许外界使用jQuery框架。但是,外界只能访问jQuery,不能访问闭包体内其他私有变量。
至此,jQuery框架的设计模型就基本完成了,后面的工作就是根据需要使用extend()函数扩展jQuery功能。例如,在闭包体外直接引用jQuery.fn.extend()函数为jQuery扩展fontStyle插件。
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P42_10442.jpg?sign=1739344143-k7U7a5kxEBU5ELhRzS3SH3b0pRrhtHln-0-87e18f6a2d792f5a07ad4854abe5641c)
使用下面代码就可以在页面中使用这个插件了。
![](https://epubservercos.yuewen.com/A91516/15056702905210506/epubprivate/OEBPS/Images/Figure-P43_40276.jpg?sign=1739344143-dEjEurCAft8ugDwcDV2VTUsJQQ5P56tq-0-ad4b0522bfe5baa3df6cb65855bd8c4c)
上面代码与2.2.8节相同,这里不再赘述,本例完整代码请参考本节示例源代码。