Open evantianx opened 7 years ago
jQuery.fn.init( selector, context, rootjQuery )
定义 源码如下:
//省略无关代码
init: function( selector, context, rootjQuery ) {
var match, elem, ret, doc;
//...
}
参数值:
selector
接受任意类型值,但是只有undefined,DOM元素,字符串,函数,jQ对象,JS对象有效。context
指明上下文,可以不传入。rootjQuery
包含了document对象的jQuery对象。用于document.getElementById()
查找失败,selector是选择器表达式且未指定context,selector是函数的情况。
//应用场景
//document.getElementById()查找失败
return rootjQuery.find( selector )
//selector是选择器表达式且未指定 context
return ( context || rootjQuery ).find( selector )
//selector是函数
return rootjQuery.ready( selector )
//定义rootjQuery
rootjQuery = jQuery(document);
参数可以转换为false
空字符串,undefined,null这三种情况下,直接返回this。this指代一个空的jQ对象。
if( !selector ) {
return this;
}
若参数有属性nodeType,则认为selector是DOM元素。
if( selector.nodeType ) {
this.context = this[0] = selector;
this.length =1;
return this;
}
参数是字符串body
专门对查找body做了优化
if( selector === "body" && !context && document.body ) {
this.context = document;
this[0] = document.body;
this.selector = selector;
this.length = 1;
return this;
}
参数是其他字符串 首先检测selector是HTML代码还是#id
if( typeof selector === "string" ) {
//判断是HTML标签还是id
if( selector.charAt(0) === "<" && selector.charAt( selector.length-1 ) === ">" && selector.length >= 3) {
//在这种情况下我们假设它为HTML标签,跳过正则匹配
match = [ null, selector, null ]
}else {
match = quickExpr.exec (selector)
}
}
正则匹配为:
quickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/
需要注意的一点是:为了防止XSS攻击,在jQ1.63及之后版本均将以#开头的视为id
关于exec()函数: 如果 exec() 找到了匹配的文本,则返回一个结果数组。否则,返回 null。此数组的第 0 个元素是与正则表达式相匹配的文本,第 1 个元素是与 RegExpObject 的第 1 个子表达式相匹配的文本(如果有的话),第 2 个元素是与 RegExpObject 的第 2 个子表达式相匹配的文本(如果有的话),以此类推。
参数selector为单独标签
此时调用document.createElement()
创建标签对应的DOM元素
if( match && (match[1] || !context) ) {
//匹配到的是HTML标签
if(match[1]) {
//修正context和doc
context = context instanceof jQuery? context[0] : context;
doc = ( context ? context.ownerDocument || context : document )
//处理单独标签的情况
ret = rsingleTag.exec( selector )
if( ret ) {
if(jQuery.isPlainObject( context )) {
//把创建好的元素放在数组中是为了后面调用merge()方法
selector = [document.createElement(ret[1])];
jQuery.fn.attr.call (selector, context, true);
}else {
selector = [doc.createElement(ret[1])];
}
}
}
}
//匹配单独标签的情况
rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/
selector是复杂HTML代码
此时调用innerHTML
创建DOM元素
}else {
ret = jQuery.buildFragment( [ match[1] ], [ doc ] );
selector = (ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment).childNodes;
}
return jQuery.merge (this, selector)
若HTML代码满足缓存条件,则在使用转换后的DOM元素时必须先负值一份再使用,否则直接使用。
selector为"#id",且未指定context
此时调用document.getElementById()
查找DOM元素
}else {
elem = document.getElementById( match[2] );
//检查父节点是否存在主要是因为黑莓4.6会返回已经不存在于文档中的节点
if( elem && elem.parentNode ) {
// 这里主要是因为在IE6,IE7及某些版本的Opera下,可能会按照属性name查找,而非ID
//在这种情况下,就调用自身的find方法来查找
if( elem.id !== match[2] ) {
return rootjQuery.find( selector );
}
//将元素直接插入jQ对象
this.length = 1;
this[0] = elem;
}
this.context = document;
this.selector = selector;
return this;
}
}else if ( !context || context.jquery ) {
return ( context || rootjQuery ).find( selector );
}
//以下等同于: $(context).find(expr)
}else {
return this.constructor( context ).find( selector );
}
selector为函数
} else if( jQuery.isFunction( selector ) ) {
return rootjQuery.ready (selector)
}
selector为jQ对象
if(selector.selector !== undefined) {
this.selector = selector.selector;
this.context = selector.context;
}
selector是任意其他值
return jQuery.makeArray(selector, this);
}
jQuery.buildFragment( args, nodes, scripts )
原理
首先创建一个文档片段DocumentFragment,然后调用方法jQuery.clean( elems, context, fragment, scripts )
将HTML代码转换为DOM元素,并存储在创建的文档片段中。
定义
jQuery.buildFragment = function (args, nodes, scripts) {
参数:
args
数组,含有待转换为DOM元素的HTML代码
nodes
数组,含有文档对象,jQ对象或DOM元素,用于修正创建文档片段DocumentFragment的文档对象
scripts
数组,用于存放HTML代码中的script元素,会把该参数传递给方法jQuery.clean()
,后者将HTML代码转换为DOM元素后会提取其中的script元素并存入数组scripts。
定义局部变量,修正doc
var fragment, cacheable, cacheresults, doc,
first = args[0];
//修正doc
if( nodes && nodes[0] ) {
doc = nodes[0].ownerDocument || nodes[0]
}
//尽管如此,doc仍然可能不是文档对象
//比如,传入$('<div></div>', {'class': 'test'})时,doc为{'class': 'test'},此时还需验证
if( !doc.createDocumentFragment ) {
doc = document;
}
尝试从缓存对象jQuery.fragments
中读取缓存的DOM元素
if( args.length === 1 && typeof first === "string" &&
first.length < 512 &&
doc === cocument &&
first.charAt(0) === "<" &&
!rnocache.test(first) &&
(jQuery.support.checkClone || !rchecked.test(first)) &&
(jQuery.support.html5Clone || !rnoshimcache.test(fiest)) {
cacheable = true;
cacheresults = jQuery.fragments[first];
//缓存值为不为空且不为1时,表示读取到的是文档片段,其中包含了DOM元素
if( cacheresults && cacheresults !==1) {
fragment = cacheresults;
}
}
可以看出,可缓存条件为:
数组args长度为1,且第一个元素为字符串,即args中只有一段HTML代码
HTML代码长度小于512(1/2kb)
文档对象doc为当前文档对象,即只缓存当前文档创建的DOM元素,不缓存其他iframe的
HTML代码以左尖括号开头,即只缓存DOM节点,不缓存文本节点
HTML代码中不能含有下列标签: <script>
,<object>
,<embed>
,<option>
,<style>
rnocache = /<(?:script|object|embed|option|style)/i
当前浏览器可以正确克隆单选按钮和复选框的选中状态checked或者未被选中
//checked = "checked" 或者 checked
rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i
当前浏览器可以正确克隆HTML5元素或者HTML代码中不包含HTML5标签
var nodeNames = "abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|" +
"header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",
rnoshimcache = new RegExp("<(?:" + nodeNames + ")", "i"),
转换HTML代码为DOM元素
//!fragment为true有三种情况:
// 1.不符合缓存条件
// 2.符合条件,是第一次转换,不存在对应缓存
// 3.符合条件,是第二次转换,对应缓存值为1
if( !fragment ) {
fragment = doc.createDocumentFragment();
jQuery.clean( args, doc, fragment, scripts );
}
把转换后的DOM元素放入缓存对象jQuery.fragements
if( cacheable ) {
jQuery.fragments[first] = cacheresults ? fragment : 1;
}
返回文档片段和缓存状态
return {
fragment: fragment,
cacheable: cacheable
};
总结下jQuery.buildFragment()
的用法:
若不符合缓存条件,则总是会执行转换过程
若符合缓存条件,第一次转换后设置缓存值为1,第二次转换后设置为文档片段,第三次及之后则从缓存中读取
jQuery.clean( elems, context, fragment, scripts )
原理
将HTML代码转换为DOM元素,并提取其中的script元素。
首先创建一个临时的div元素,并将其插入一个安全文档片段(指能正确渲染HTML5元素的文档片段,通过这个操作可以教会浏览器正确渲染HTML5元素)中,之后把HTML代码赋值给div的innerHTML属性,浏览器会自动生成DOM元素,最后解析div的子元素即可得到转换后的DOM元素
定义
clean: function (elems, context, fragment, scripts){
参数:
elems
数组,包含了待转换的HTML代码
context
文档对象,在(二)方法jQuery.buildfragment()
中被修正为正确的文档对象doc,之后会调用它的方法createTextNode()
创建文本节点,调用方法createElement()
创建临时div元素。
fragment
文档片段,作为存放转换后的DOM元素的占位符,已在(二)中被创建。
scripts
数组,用于存放转换后的DOM元素中的script元素
修正context
var checkScriptType;
context = context || document;
if( typeof context.createEl;ement === "undefined" ) {
context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
}
疑点:既然在jQuery.buildFragment()
中已经修正过context并传给了jQuery.clean()
,为何还要再修正一遍?
这是因为在DOM操作模块的方法.before()
和.after()
中,将会直接调用jQuery.clean()
转换HTML代码为DOM元素,且只传入elems,不会传入context,fragment和scripts。
//jQuery.fn.before
before: function () {
var set = jQuery.clean( arguments );
//...
}
//jQuery.fn.after
after: function() {
set.push.apply( set, jQuery.clean(arguments) );
//...
}
遍历待转换的HTML代码数组elems
var set = [],j;
for (var i = 0,elem; (elem = elems[i]) != null; i++) {
if( typeof elem === "number" ) {
elem += "";
}
//过滤空字符串的情况
if( !elem) {
continue;
}
//将HTML转换为DOM
if( typeof elem === "string" )
}
有几个点需要注意:
(elem=elems[i]) != null
来作为循环条件,非常巧妙!
一般来说我们会直接判断i<elems.length
,然后在代码中再判断elems[i]
是否为空,这样做会有些冗余。如代码所写,简明扼要,值得学习 ;
另外 使用!=
判断不仅过滤了null 和uindefined,也保留下了整型数字0elem += ""
将其转换为字符串,这里将其转换为字符串是为了简化之后对elem有效性和类型判断创建文本节点
若HTML代码中不包含标签,字符代码和数字代码,则调用document.createTextNode()
创建文本节点:
if(! rhtml.test (elem)) {
//createTextNode原生方法不会对字符串参数进行转义解析,故不能传字符或数字代码
//innerHTML可以
elem = context.createTextNode (elem);
} else {
正则rhtml检测代码中是否含有标签(以<开头),字符代码(以&开头)或数字代码(以&#开头):
rhtml = /<|$#?\w+;/
修正自关闭标签
// fix "XHTML"-style tags in all browsers
elem = elem.replace (rxhtmlTag, "<$1></$2>")
即修正本来不是自关闭标签而被写为自关闭标签的标签,有点绕口,就比如说<div></div>
被写成了<div/>
,就需要来修正。
正则表达式rxhtmlTag:
rxhtmlTag=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig
话说我没懂为啥这里要加
:
创建一个临时div元素
//提取标签部分,删除前导空白符和左尖括号
var tag = ( rtagName.exec (elem) || ["", ""] )[1].toLowerCase(),
wrap = wrapMap [tag] || wrapMap._default,
//依据depth的值来剥去父元素
depth = wrap[0],
div = context.createElement("div"),
正则rtagName:
rtagName = /<([\w:]+)/
上述代码中可以看到wrap
这个变量是从wrapMap
中提取出来的,我们看看它长什么样:
wrapMap = {
option: [ 1, "<select multiple='multiple'>", "</select>" ],
legend: [ 1, "<fieldset>", "</field>" ],
thead: [1, "<table>", "</table>"],
tr: [2, "<table><tbody>", "</tbody></table>"],
td: [3, "<table><tbody><tr>", "</tr></tbody></table>"],
col: [2, "<table><tbody></tbody><colgroup>", "</colgroup></table>"],
area: [1, "<map>", "</map>"],
_default: [0, "", ""]
},
safeFragment = createSafeFragment( document );
wrapMap.optgroup = wrapMap.option;
wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup =wrapMap.caption = wrapMap.thead;
wrapMap.th = wrapMap.td;
//IE9以下不能序列化<link>和<script>
if (!jQuery.support.htmlSerialize) {
wrapMap._default = [1, "div<div>", "</div>"]
}
关于这个wrapMap
wrapMap
中每一个数组元素依次为: 包裹深度,包裹的父标签,父标签对应的关闭标签<select multiple="multiple">
中。若包含在单选的<selector>
中,创建的第一个option元素的selected属性会被浏览器设置为true。IE9以下的浏览器中不能序列化<link>
和<script>
var div = document.createElement("div");
//script不被包裹
div.innerHTML = "<script><script>";
console.log(div.getElementsByTagName("script").length); // 0
//script被包裹
div.innerHTML = "div<div><script></script></div>";
console.log(div.getElementsByTagName("script").length); // 1
被包裹之后输出正确,故源码中对这一情况也进行了修正。
将临时div元素插入安全文档片段中
if (context === document) {
safeFragment.appendChild(div)
}else {
createSafeFragment(context).appendChild(div)
}
所谓安全文档片段,是指不支持HTML5的浏览器也能够正确解析和渲染未知的HTML5标签,即能够正确构建DOM树,并可以为之设置样式。
如何实现呢?
在使用未知标签之前调用document.createElement('未知标签')
创建一个对应的DOM元素,这样就教会浏览器如何正确解析和渲染这个未知标签。
createSafeFragment()
的实现:
function createSafeFragment ( document ) {
var list = nodeNames.split("|"),
safeFrag = document.createDocumentFragment();
if (safeFrag.createElement) {
while (list.length) {
safeFrag.createElement(
list.pop()
)
}
}
return safeFrag;
}
var nodeNames = //...定义HTML5的所有标签,用"|"分隔
利用浏览器的innerHTML机制将HTML代码转换为DOM元素 首先为HTML代码包裹必要的父标签,然后赋值给临时div元素的innerHTML属性,从而将HTML代码转换为DOM元素,之后层层剥去包裹的父元素,得到转换后的DOM元素
div.innerHTML = wrap[1] + elem + wrap[2];
while (depth--) {
div = div.lastChild
}
移除IE6/7自动插入的空tbody元素
if(!jQuery.support.tbody) {
var hasBody = rtboby.test(elem),
//HTML代码中含有table标签,没有tbody标签
tbody = tag ==="table" && !hasBody?
div.firstChild && div.firstChild.childNodes :
//HTML代码中没有tbody标签,但是被包裹了table标签
wrap[1] === "<table>" && !hasBody ?
div.childNodes :
[];
//移除空tbody元素
for (j = tbody.length -1; j>=0; --j) {
//数组tbody中还有可能有除tbody之外的thead,tfoot,colgroup,caption等元素
if( jQuery.nodeName(tbody[j], "tbody") && !tbody[j].childNodes.length ){
tbody[j].parentNode.removeChild(tbody[j])
}
}
}
这个三元表达式真是厉害!
插入IE 6/7/8自动删除的前导空白符
if (!jQuery.support.leadingWhitespace && rleadingWhitespace.test(elem)) {
//将被删除的前导空白符添加到div第一个子元素的前面
div.insertBefore(
context.createTextNode( rleadingWhitespace.exec(elem)[0] ),
div.firstchild
)
}
IE6/7/8中,设置innerHTML属性时,浏览器会自动剔除前导空白符。
正则rleadingWhitespace:
rleadingWhitespace = /^\s+/
取到转换后的DOM元素集合
elem = div.childNodes;
在IE6/7中修正复选框和单选框的选中状态
var len;
if (!jQuery.support.appendChecked) {
if( elem[0] && typeof (len = elem.length) === "number" ) {
for (j=0; j< len; j++){
findInputs(elem[j])
}
}else {
findInputs(elem)
}
}
在IE6/7中,复选框和单选按钮在插入DOM树后,其选中状态checked会丢失
findInputs(elem)
的实现:
function fixDefaultChecked (elem) {
if(elem.type === "checkbox" || elem.type === "radio") {
elem.defaultChecked = elem.checked;
}
}
function findInputs (elem) {
var nodeName = (elem.nodeName || "").toLowerCase();
if( nodeName === "input") {
fixDefaultChecked(elem);
}else if (nodeName !== "script" && typeof elem.getElementsByTagName !== "undefined") {
jQuery.qrep(elem.getElementsByTagName("input", fixDefaultChecked));
}
}
合并转换后的DOM元素
if(elem.nodeType) {
ret.push (elem);
}else {
ret = jQuery.merge (ret, elem);
}
至此,数组elems中的所有HTML代码都已转换为DOM元素,并合并到了ret数组中
传入文档片段fragment的情况 若传入了文档片段,则遍历数组ret,提取所有合法的script元素存入数组scripts,并把其他元素插入文档片段fragment
if (fragment) {
//用来检测script元素是否可执行
checkScriptType = function (elem) {
return !elem.type || rscriptType.test (elem.type)
};
for (i =0; ret[i]; i++) {
if( scripts && jQUery.nodeName (ret[i], "script") &&
(!ret[i].type || ret[i].type.toLowerCase() === "text/javascript")){
scripts.push (ret[i].parentNode? ret[i].parentNode.removeChild(ret[i]) : ret[i])
}else {
if(ret[i].nodeType === 1) {
var jsTags = jQuery.qrep(ret[i].getElementsByTagName("scripts"), checkScriptType)
ret.splice.apply(ret, [i+1, 0].concat(jsTags));
}
fragment.appendChild(ret[i])
}
}
}
返回转换后的DOM元素数组
return ret
若传入了fragment和scripts,则调用jQuery.clean()的代码从文档片段中读取转换后的DOM元素,从数组scripts中读取合法的scripts元素; 若没传,则使用ret
jQuery.extend()
/jQuery.fn.extend()
用于合并两个或多个对象的属性到第一个对象
定义
jQuery.extend([deep], target, object1[, objectN])
jQuery.fn.extend([deep], target, object1[, objectN])
//由于参数个数不确定,故不写参数
jQuery.extend = jQuery.fn.extend = function () {
参数:
deep
可选参数,布尔值,表示是否进行深度合并(递归合并),默认为false(浅复制)。
target
目标对象。
object1...object N
源对象
若提供了两个或更多的对象,则所有源对象的属性会被合并到目标对象;
若只提供了一个对象,则意味着target被忽略,jQuery
/jQuery.fn
被当作目标对象。通过这种方式可以在这两个对象上添加属性和方法,这也是jQuery中其他模块的实现方法。
定义局部变量
var options, name, src, copy, copyIsArray, clone,
target = arguments[0] || {},
i = 1,
length = arguments.length,
deep = false;
修正目标对象target,源对象起始下标i
//处理深复制情况
if (typeof target === "boolean") {
deep = target;
target = arguments[1] || {};
//源对象下标跳过布尔值和目标对象target, 从2开始
i = 2;
}
//处理target为一个字符串或者其他类型,而非对象,则统一换成空对象{}
if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
target = {};
}
//只有一个参数传入时扩展jQuery本身
if (length === i) {
target = this;
--i;
}
最后的判断length === i
时,有两种情况:
extend(object)
传入一个参数extend(deep, object)
传入两个参数,第一个是布尔值,此时i=2逐个遍历源对象
for (; i<length; i++) {
i为源对象开始的下标
//处理非null、undefined的值
if (options = arguments[i] != null) {
//扩展基本对象
for (name in options) {
}
//进入for in循环
src = target [name];
copy = options [name];
//防止死循环 if (target === copy) { continue; }
//若合并的属性为对象或数组,则递归复制而非简单复制(deep为true的情况下) if(deep && copy&&(jQuery.isPlainObject(copy)) || (copyIsArray = jQuery.isArray(copy))){ if(copyIsArray) { copyIsArray = false; clone = src && jQuery.isArray(src) ? src : []; }else { clone = src && jQuery.isPlainObject(src)? src : {}; }
//不要浅拷贝,要深复制 target[name] = jQuery.extend (deep, clone, copy) //两种情况: //1.copy为非对象或数组类型且不为undefined //2.deep为false }else if( copy !== undefined) { target [name] = copy; }
这里防止死循环的原因是: 假设源对象上某个属性值为目标对象,而目标对象上也有个属性值为目标对象,这样的话递归就会不断循环,没有尽头:
```js
var o = {};
o.n1 = o;
//若没有那个判断,则下列语句会出错
$.extend( true, o, {n2: o} );
jQuery.fn = jQuery.prototype = {
constructor: jQuery,
init: function(selector, context, rootjQuery){},
selector: "",
jQuery: "1.7.1",
length: 0,
size: function(){},
toArray: function(){},
get: function(){},
pushStack: function(elems, name, selector) {},
each: function(callback, args){},
ready: function(fn){},
eq: function(){},
first: function(){},
last: function(){},
slice: function(){},
map: function(){},
end: function(){},
push: push,
sort: [].sort,
splice: [].splice
}
.selector
,.jquery
,.length
,.size()
selector: "",
jquery: "1.7.1",
length: 0,
size: function() {
return this.length
}
.selector
记录选择器表达式,主要是方便调试
$('div').find('p').selector; // "div p"
.jquery
记录版本号
.length
表示当前jQuery对象中元素的个数
.size()
返回当前jQuery对象中元素的个数,功能上等价于属性.length
,但是有函数调用开销。
.toArray()
/.get([index])
toArray()
toArray()
将当前jQuery对象转换为真正的数组,借用了slice()
toString = Object.prototype.toString;
hasOwn = Object.prototype.hasOwnProperty;
push = Array.prototype.push;
slice = Array.prototype.slice;
trim = String.prototype.trim;
indexOf = Array.prototype.indexOf;
//利用slice()将当前jQuery对象转换为了数组
toArray: function() {
return slice.call(this,0)
}
get([index])
返回当前jQuery中指定位置的元素或包含了全部元素的数组。没有参数,则调用toArray()
返回包含所有元素的数组;指定了参数index,则返回一个单独的元素。
get: function(num) {
return num == null?
//未传入参数则返回全部元素的数组
this.toArray():
//传入参数判断参数为正还是负
(num < 0? this[this.length + num] : this [num]);
}
each(function(index, Element))
/jQuery.each(collection, callback(indexInArray, valueOfElement))
each(function(index, Element))
each: function (callback, args) {
return jQuery.each(this, callback, args)
}
this总是指向当前元素
jQuery.each( collection, callback(indexInArray, valueOfElement) )
each: function (object, callback, args) {
var name, i =0,
length = object.length,
//判断object是对象还是数组
isObj = length === undefined || jQuery.isFunction (object) ;
if(args) {
if( isObj) {
for (name in object) {
//使用apply指定this关键字所引用的对象
if( callback.apply(object[name], args) === false ) {
break
}
}
}else {
for (; i< length;) {
if (callback.apply (object [i++], args) === false) {
break;
}
}
}
}else {
if(isObj) {
for(name in object) {
if(callback.call(object[name], name, object[name]) === false) {
break;
}
}
}else {
for (; i<length;) {
if (callback.call(object[i], i, object[i++]) === false) {
break;
}
}
}
}
//方便链式调用
return object;
}
参数:
object
待遍历的对象或数组
callback
回调函数,会在数组的每个元素或对象的每个属性上执行
args
传给回调函数callback的参数数组,可选。若未传入,则执行回调函数时会传入两个参数(下标或属性名,对应的元素或属性值);若传入了参数args,则只会把该参数传给回调函数
map(callback(index, domElement))
/jQuery.map(arrayOrObject, callback(value, indexOrKey))
map(callback(index, domElement))
map: function( callback ) {
return this.pushStack (jQuery.map (this, function(elem, i) {
return callback.call (elem, i ,elem)
}))
}
用来遍历当前jQuery对象,在每个元素上执行回调函数,并将回调函数的返回值放入一个新jQ对象中。该方法常用于获取或设置DOM元素集合的值。
jQuery.map(arrayOrObject, callback(value, indexOrKey))
map: function (elems, callback, arg) {
var value, key, ret = [],
i = 0,
length = elems.length,
//在jQuery中将对象和数组视为一类,话说上面的each的判断怎么没有这一条?
isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" &&
//类数组
((length > 0 && elems[0] && elems[length-1])
//空数组
|| length === 0
//数组
|| jQuery.isArray (elems));
if(isArray) {
for (; i< length; i++) {
value = callback(elems[i], i, arg);
if(value != null) {
ret[ret.length] = value
}
}
}else {
for (key in elems) {
value = callback (elems[key], key, arg);
if( value != null ) {
ret[ret.length] = value
}
}
}
//扁平化数组(这种方法只适用于单层嵌套如 [1,2,[3,4]] 这种 )
return ret.concat.apply ([], ret);
}
参数:
elems:
待遍历数组或对象
callback
回调函数,会在数组的每个元素或对象的每个属性上执行。执行时传入两个参数: 数组元素或属性值,下标或属性名。
arg
仅限于jQuery内部使用。若调用jQuery.map()
时传入了参数arg,那么该参数就会被传给回调函数callback。
pushStack(element, name, arguments)
创建一个新的空jQuery对象,然后把DOM元素集合放入这个jQuery对象中,并保留对当前jQuery对象的引用。
是很多方法如find()
,add()
等的支持。
pushStack : function( elems, name, selector ) {
//创建一个空jQuery对象
var ret = this.constructor();
if(jQuery.isArray(elems)) {
push.apply(ret, elems)
}else {
jQuery.merge( ret, elems )
}
//保留引用
ret.prevObject = this;
ret.context = this.context;
if(name === "find") {
ret.selector = this.selector + (this.selector? " ": "") + selector;
}else if (name) {
ret.selector = this.selector + "." + name + "(" + selector + ")";
}
return ret;
}
end()
结束当前链条中最近的筛选操作,并将匹配元素集合还原为之前状态。
end: function() {
return this.prevObject || this.constructor(null)
}
总结: pushStack()
用于入栈,end()
用于出栈。
eq(index)
/first()
/last()
/slice(start[, end])
方法调用链: first()/last() ——>eq(index)——>slice(start[,end])——>pushStack(elements, name, arguments)
eq: function (i) {
//将字符串转换为数字
i = +i;
return i === -1?
this.slice(i):
this.slice(i, i+1);
},
first: function() {
return this.eq(0);
},
last: function() {
return this.eq(-1);
} ,
slice: function() {
//先借用数组方法slice()从当前jQuery对象中获取指定范围的子集(数组),
//再调用方法pushStack()把子集转换为jQuery对象
//同时也通过prevObject保留了对当前jQ对象的引用
return this.pushStack(slice.apply (this, arguments), "slice", slice.call(arguments).join(","))
}
push(value,...)
/sort([orderfunc])
/splice(start,deleteCount, value,...)
//仅在内部使用
push: push,
sort: [].sort(),
splice: [].splice
重要的静态属性和方法。
jQuery.extend({
noConflict: function( deep ) {},
isReady: false,
readyWait: 1,
holdReady: function(hold) {},
ready: function(wait){},
bindReady: function(){},
isFunction: function(obj){},
isArray: Array.isArray || function(obj) {},
isWindow: function(obj) {},
isNumeric: function(obj){},
type: function(obj){},
isPlainObject: function(obj){},
isEmptyObject: function(obj){},
error: function(msg){},
parseJSON: function(data) {},
parseXML: function(data) {},
noop: function(){},
globalEval: function(data){},
camelCase: function(string){},
nodeName: function(elem, name) {},
each: function(object, callback, args),
//trim为String.prototype.trim
trim: trim?function(text){}: function(text){},
makeArray: function(array, results){},
inArray: function(elem, array,i){},
merge: function(first, second){},
qrep: function(elems, callback, inv){},
map: function(elems, callback, arg){},
quid: 1,
proxy: function (fn, context) {},
access: function( elems, key, value, exec, fn, pass) {},
now: function() {},
uaMatch: function(ua) {},
sub: function(){},
browser: {}
})
jQuery.noConflict([removeAll])
用于释放jQuery对全局变量$的控制权,可选参数表示是否释放对全局变量jQuery的控制权。
//保留当前引用,是在jQuery被赋给全局之前调用
_jQuery = window.jQuery,
_$ = window.$,
jQuery.extend({
noConflict: function(deep) {
if(window.$ === jQuery) {
window.$ = _$;
}
if(deep && window.jQuery === jQuery) {
window.jQuery = _jquery;
}
return jQuery;
}
})
//代码最后将jQuery和$暴露给全局
window.jQuery = window.$ = jQuery;
jQuery.isFunction(obj)
/jQuery.isArray(obj)
这两个方法都依赖于jQuery.type(obj)
isFunction: function(obj) {
return jQuery.type(obj) === "function";
},
isArray: Array.isArray || function(obj) {
return jQuery.type(obj) === "array";
}
jQuery.type(obj)
用于判断参数类型:当参数是undefined或null时,返回"undefined"或"null";当参数是JS内部对象,则返回对应的字符串名称;其他情况一律返回“object”。
type: function(obj) {
return obj === null ?
String(obj):
class2type[toString.call(obj)] || "object";
}
String(obj)
相当于调用toString(obj)
,下面来分析下class2type这一行:
toString = Object.prototypr.toString,
class2type = {};
jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name){
class2type["[object" + name + "]"] = name.toLowerCase();
})
jQuery.isWindow(obj)
判断传入的参数是否为window对象(通过检测是否存在特征属性setInterval来实现):
isWindow: function(obj) {
return obj && typeof obj === "object" && "setInterval" in obj;
}
在1.7.2版本中改为了对window属性的检测,该属性为Window对象对自身的引用。
jQuery.isNumeric(value)
用于判断传入的参数是否为数字或者“类数字”:
isNumeric: function (obj) {
return !isNaN(parseFloat(obj)) && isFinite(obj)
}
jQuery.isPlainObject(object)
用于判断传入的参数是否为纯粹的对象(即由对象直接量{}或new Object()创建的对象)
isPlainObject: function(obj) {
//obj可转换为false || toString结果为[object Object] || obj为DOM元素 || obj为window对象
if( !obj || jQuery.type(obj) != "object" || obj.nodeType || jQuery.isWindow(obj)) {
return false;
}
try {
//存在constructor属性
if(obj.constructor &&
//constructor为继承属性
!hasOwn.call(obj, "constructor") &&
//obj.constructor.prrototype的isPrototypeOf为继承属性
!hasOwn.call(obj.constructor.prototype, "isPrototypeOf")
){
return false;
}
}catch(e) {
//IE8,9会抛出错误
return false
}
var key;
for (key in obj) {}
//在枚举的时候会先枚举非继承属性,最后枚举继承属性;
//所以检测最后一个属性是否为继承属性即可
return key === undefined || hasOwn.call(obj, key);
}
jQuery.isEmptyObject(object)
用于检测对象是否为空(即不包含属性)
isEmptyObject: function(obj) {
for(var name in obj) {
return false;
}
return true;
}
jQuery.parseJSON(data)
接受JSON字符串,返回解析后的JS对象。
parseJSON: function(data){
//非法参数一律返回null
if( typeof data !== "string" || !data ) {
return null
}
//去除首尾空格(在IE6,7中不移除则无法正确解析)
data = jQuery.trim(data);
//若存在JSON.parse()方法则调用此方法解析
if(window.JSON && window.JSON.parse) {
return window.JSON.parse(data)
}
//不支持JSON.parse()的浏览器
if(rvalidchars.test (data.replace(rvalidescape, "@")
.replace(rvalidtokens, "]")
.replace(rvalidbraces, ""))) {
//注意这里的写法
return (new Function("return " + data)) ();
}
jQuery.error("Invalid JSON: " + data);
}
正则匹配表达式如下:
rvalidchars = /^[\],:{}\s]*$/,
rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,
rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,
rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g,
此处是参照json2.js的判断规则
jQuery.parseXML(data)
接受XML字符串,返回解析后的XML文档
使用浏览器原生XML解析函数实现: 在IE9+及其他浏览器中使用DOMParser
对象解析;在IE9以下浏览器中使用ActiveXObject
对象解析
parseXML: function(data) {
var xml, tmp;
try {
if(window.DOMParser) {
tmp = new DOMParser();
xml = tmp.parseFromString(data, "text/xml");
}else {
xml = new ActiveXObject("Microsoft.XMLDOM");
xml.async = "false";
//成功返回true;
//失败返回false,不抛出异常,并设置根节点documentElement为null
xml.loadXML(data);
}
}catch(e) {
xml = undefined;
}
//在IE以外的浏览器中,若DOMParser解析失败,不会抛出异常
//而是返回一个包含了错误信息的文档对象<parsererror>
if(!xml || !xml.documentElement || xml.getElementsByTagName("parsererror").length) {
jQuery.error("Invalid XML: " + data)
}
return xml;
}
jQuery.globalEval(code)
用于在全局作用域中执行JS代码。
在IE中可以调用方法execScript()
让JS代码在全局作用域中执行;在其他浏览器中,则需要在一个自调用匿名函数中调用eval()
执行JS代码(确保执行环境为全局作用域)
globalEval: function (data) {
if(data && rnotwhite.test( data )) {
(window.execScript || function(data) {
window["eval"].call(window, data);
})(data);
}
}
jQuery.camelCase(string)
转换连字符式的字符串为驼峰式:
rdashAlpha = /-([a-z]|[0-9])/ig,
//在IE中"-ms-transform"对应"msTransform"而非MsTransform
rmsPrefix = /^-ms-/,
fcamelCase = function(all, letter) {
return (letter + "").toUpperCase()
},
camelCase: function(string){
return string.replace(rmsPrefix, "ms-").replace(rdashAplpha, fcamelCase)
}
jQuery.nodeName(elem, name)
用于检查DOM元素的节点名称(即属性nodeName)与指定的值是否相等,检查时忽略大小写。
nodeName: function (elem, name) {
//HTML文档元素的nodeName始终返回大写形式;
//XML文档区分大小写,返回源码中的形式
//所以要使用toUpperCase()
return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase();
}
jQuery.trim(str)
用于移除字符串开头和结尾的空白符。 传入参数是null或undefined,则返回空字符串l;传入参数为对象,则首先获取对象的字符串表示,然后移除开头和结尾的空白符并返回。
rnotwhite = /\S/,
trimLeft = /^\s+/,
trimRoght = /\s+$/,
trim = String.prototype.trim,
//在IE9以下的浏览器中,\s不匹配不间断空格\xA0
//此时加上即可
if(rnotwhite.test("\xA0")) {
trimLeft = /^[\s\xA0]+/;
trimRight = /[\s\xA0]+$/
}
//存在原生方法trim时使用原生方法
//否则使用正则匹配替换
trim: trim?
function(text) {
return text == null?
"":
trim.call(text)
}:
function(text) {
return text == null ?
"":
text.toString().replace(trimLeft, "").replace(trimRight, "")
}
所谓的\xA0,即为不间断空格(non-breaking space)
jQuery.makeArray(obj)
将一个类数组对象转换为真正的数组。 在jQuery内部调用时还可传入第二个参数,第一个参数中的元素会被并入第二个参数,最后返回第二个参数,此时返回的值不一定为数组。
push = Array.prototype.push,
makeArray: function(array, results) {
var ret = results || [];
if(array != null) {
var type = jQuery.type(array);
//满足下列条件之一时认为不是数组,也不是类数组对象
//1.不存在length属性
//2.字符串,length为其长度
//3.函数,length为形参个数
//4.window对象,length返回窗口中frame/iframe个数
//5.正则对象,黑莓4.7中,正则对象也有length属性
if(array.length == null
|| type === "string"
|| type === "function"
|| type === "regexp"
|| jQuery.isWindow(array) ){
//由于ret不一定为数组,故使用push.call而不是ret.push
push.call(ret, array)
}else {
jQuery.merge (ret, array)
}
}
return ret;
}
jQuery.inArray(value, array[,fromIndex])
在数组中查找指定的元素并返回下标,没有则返回-1(优先采用indexOf()方法)
inArray: function(elem, array, i) {
var len;
if( array ) {
if(indexOf) {
return indexOf.call(array, elem, i);
}
len = array.length;
//修正i
i = i? (i<0 ? Math.max(0, len+i) : i) : 0;
for(; i< len; i++){
if(i in array && array[i] === elem) {
return i
}
}
}
return -1;
}
jQuery.merge(first, second)
用于合并两个数组的元素到第一个数组中(可以是数组或者类数组对象)
merge: function(first, second) {
var i = first.length,
j = 0;
if(typeof second.length === "number") {
for (var l = second.length; j<1; j++){
first[i++] = second[j]
}
}else {
//此时second为含有连续整型属性的对象如{0: 'a',1: 'b'}
while (second[j] !== undefined){
first[i++] = second[j++]
}
}
//对于非数组需要手动修正其length
first.length = i;
return first;
}
jQuery.grep(array, function(elementOfArray, indexInArray)[, invert])
用于查找数组中满足过滤函数的元素,原数组不会受影响。 当未传入invert或传入invert且为false时,返回一个满足回调函数的元素数组;true时,返回一个不满足回调函数的元素数组。
grep: function(elems, callback, inv) {
var ret = [], retVal;
inv = !!inv;
for( var i = 0, length = elems.length; i< length; i++) {
retVal = !!callback(elems[i], i);
if( inv !== retVal) {
ret.push(elems[i])
}
}
return ret;
}
jQuery.guid
/jQuery.proxy(function, context)
jQuery.guid
全局计数器,用于jQuery事件模块和缓存模块。初始为1,使用时自增1。
guid: 1,
uuid: 1,
elem[internalKey] = id = ++jQuery.uuid;
handler.guid = jQuery.guid++;
jQuery.proxy(function, context)
接受一个函数,返回一个新函数。新函数总有特定的上下文。
jQuery.proxy(function, context)
改变某个函数的上下文jQuery.proxy(context, name)
参数name是context的属性。
proxy: function(fn, context) {
//当context为一个字符串时,此时传入参数格式应为(context, name)
//故进行修正
if(typeof context === "string"){
var tmp = fn[context];
context = fn;
fn = tmp
}
if(!jQuery.isFunction(fn)){
return undefined;
}
//处理多余参数
var args = slice.call(arguments, 2),
proxy = function() {
return fn.apply(context, args.concat(slice.call(arguments)))
}
proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++;
return proxy;
}
相同的唯一标识将代理函数和原始函数关联了起来。此时,在jQuery事件系统中,若为DOM元素绑定了事件监听函数的代理函数,当移除事件时,即时传入的是原始函数,jQuery也能通过唯一标识guid移除正确的函数。
jQuery.access(elems, key, value, exec, fn(elem, key, value), pass)
为集合中的元素设置一个或多个属性值,或者读取第一个元素的属性值。若设置的属性值为函数且exec为true,还会执行函数并取其返回值作为属性值。
为attr()
/prop()
/css()
提供支持
attr: function (name, value) {
return jQuery.access(this, name, value , true, jQuery.attr)
},
prop: function (name, value) {
return jQuery.access(this, name, value , true, jQuery.prop)
},
jQuery.fn.css = function(name, value) {
//...
return jQuery.access(this, name, value, true, function(elem, name, value) {
return value !== undefined?
jQuery.style(elem, name, value):
jQuery.css(elem, name);
})
}
方法jQuery.access()
源码如下:
access: function(elems, key,value, exec, fn, pass) {
var length = elems.length;
if(typeof key === "object"){
for(var k in key){
jQuery.access(elems, k, key[k], exec, fn, value);
}
return elems
}
//当key为对象时,value=undefined
if(value !== undefined) {
exec = !pass && exec && jQuery.isFunction(value);
for(var i = 0; i< length; i++){
fn(elems[i], key, exec? value.call(elems[i],i, fn(elems[i],key)):value, pass);
}
return elems
}
return length? fn(elems[0], key):undefined
}
jQuery.error(message)
/jQuery.noop()
/jQuery.now()
error: function(msg){
throw new Error(msg)
},
noop: function(){},
now: function(){
return (new Date()).getTime();
}
jQuery.uaMatch(ua)
/jQuery.browser
jQuery.browser
属性值结构如下:
//jQuery.browser
{
webkit/opera/msie/mozilla: true,
version: '版本号'
}
navigator
是全局对象window的属性,指向一个Navigator对象,包含了正在使用的浏览器的信息;navigator.userAgent
包含了浏览器用于HTTP请求的用户代理头(User-Agent)的值。
rwebkit = /(webkit)[ \/]([\w.]+)/,
ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/,
rmsie = /(msie)([\w.]+)/,
rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/,
userAgent = navigator.userAgent,
uaMatch: function(ua) {
ua = ua.toLowerCase();
var match = rwebkit.exec(ua) ||
ropera.exec(ua) ||
rmsie.exec(ua) ||
ua.indexOf("compatible") < 0 && rmozilla.exec(ua) ||
[];
return {browser: match[1] || "", version: match[2] || "0"};
},
browser: {}
));
browserMatch = jQuery.uaMatch(userAgent);
if(browserMatch.browser) {
jQuery.browser[browserMatch.browser] = true;
jQuery.browser.version = browserMatch.version;
}
总体结构
有几个点需要注意一下:
自调用匿名函数
使用自调用匿名函数可以创建一个封闭的作用域,不污染全局,只暴露一个全局变量
jQuery
,其余全部为私有。为何要传入
window
和undefined
这两个变量?由于
window
为全局对象,作为函数参数传入后,可使其变为局部变量,这样jQ在访问window
对象时,就不必回退到顶层作用域去查找了;同时,这样方便代码的压缩。undefined
道理同上。另外传入它也是避免它的值被修改。为何要在构造函数jQuery()内部用new创建并返回另外一个构造函数的实例?
new一个构造函数默认会返回一个新的实例,但是若构造函数有返回值,则new所创建的对象则会被丢弃,返回值将作为new表达式的值。 因此jQ这样做的原因是:我们在创建jQuery实例时,直接写
jquery()
即可,不必加new,比较方便。既然我们使用的jQ对象是
jQuery.fn.init()
的实例,为何我们依旧可以调用很多jQuery对象原型上的方法?因为代码中有一行:
jQuery.fn.init.prototype = jQuery.fn
为何定义jQuery要使用一个自调用匿名函数?
降低构造jQuery对象模块与其他模块的耦合度
为何要在
jQuery.prototype
中重新声明constructor?