2.3 jQuery.fn.init( selector, context, rootjQuery )
2.3.1 12个分支
构造函数jQuery.fn.init()负责解析参数selector和context的类型,并执行相应的逻辑,最后返回jQuery.fn.init()的实例。参数selector和context共有12个有效分支,如表2-1所示。
表2-1 参数selector和context的12个分支
selector context 示 例
1 可以转换为false — $()
2 DOM元素 — $( document.body )
3 字符串 “body” — $('body')
4 单独标签 — $('<div>')
$('<div>',{'class': 'test'} )
5 复杂 HTML 代码 — $('<div>abc</div>')
6 “#id” undefined $('#id')
7 选择器表达式 undefined $('div p')
8 选择器表达式 jQuery 对象 $('div p', $('#id') )
9 选择器表达式 DOM 元素 $('div.foo').click( function() {
$('span', this ).addClass('bar');
} );
10 函数 — $( function(){ ... } )
11 jQuery 对象 — $( $('div p') )
12 其他任意类型的值 — $( { abc: 123 } )
$( [ 1, 2, 3 ] )
下面分析jQuery.fn.init()的源码,看看它是如何解析和处理参数selector和context的12个分支的。
2.3.2 源码分析
1.?定义jQuery.fn.init( selector, context, rootjQuery )
相关代码如下所示:
99 init: function( selector, context, rootjQuery ) {
100 var match, elem, ret, doc;
101
第99行:定义构造函数jQuery.fn.init( selector, context, rootjQuery ),它接受3个参数:
参数 selector:可以是任意类型的值,但只有undefined、DOM 元素、字符串、函数、jQuery对象、普通 JavaScript对象这几种类型是有效的,其他类型的值也可以接受但没有意义。
参数 context:可以不传入,或者传入DOM元素、jQuery对象、普通 JavaScript 对象之一。
参数rootjQuery:包含了document对象的jQuery对象,用于 document.getElement
ById()查找失败、selector是选择器表达式且未指定context、selector是函数的情况。rootjQuery 的定义和应用场景的代码如下所示:
// document.getElementById() 查找失败
172 return rootjQuery.find( selector );
// selector 是选择器表达式且未指定 context
187 return ( context || rootjQuery ).find( selector );
// selector 是函数
198 return rootjQuery.ready( selector );
// 定义 rootjQuery
916 // All jQuery objects should point back to these
917 rootjQuery = jQuery(document);
918
第100行:变量match、elem、ret、doc的功能会在接下来的分析过程中介绍。
2.?参数selector可以转换为false
参数selector可以转换为false,例如是undefined、空字符串、null等,则直接返回this,此时this是空jQuery对象,其属性length等于0。相关代码如下所示:
102 // Handle $(""), $(null), or $( undefined )
103 if ( !selector ) {
104 return this;
105 }
106
3.?参数selector是DOM元素
如果参数selector有属性nodeType,则认为selector是DOM元素,手动设置第一个元素和属性context指向该DOM元素、属性length为1,然后返回包含了该DOM元素引用的jQuery对象。相关代码如下所示:
107 // Handle $(DOMElement)
108 if ( selector.nodeType ) {
109 this.context = this[0] = selector;
110 this.length = 1;
111 return this;
112 }
113
第108行:属性nodeType声明了文档树中节点的类型,例如,Element节点的该属性值是1,Text节点是3,Comment节点是9,Document对象是9,DocumentFragment节点是11。
4.?参数selector是字符串“body”
如果参数selector是字符串“body”,手动设置属性context指向document对象、第一个元素指向body元素、属性length为1,最后返回包含了body元素引用的jQuery对象。这里是对查找字符串“body”的优化,因为文档树中只会存在一个body元素。相关代码如下所示:
114 // The body element only exists once, optimize finding it
115 if ( selector === "body" && !context && document.body ) {
116 this.context = document;
117 this[0] = document.body;
118 this.selector = selector;
119 this.length = 1;
120 return this;
121 }
122
5.?参数selector是其他字符串
如果参数selector是其他字符串,则先检测selector是HTML代码还是#id。相关代码如下所示:
123 // Handle HTML strings
124 if ( typeof selector === "string" ) {
125 // Are we dealing with HTML string or an ID?
126 if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) {
127 // Assume that strings that start and end with <> are HTML and skip the regex check
128 match = [ null, selector, null ];
129
130 } else {
131 match = quickExpr.exec( selector );
132 }
133
第126~128行:如果参数selector以“<”开头、以“>”结尾,且长度大于等于3,则假设这个字符串是HTML片段,跳过正则quickExpr的检查。注意这里仅仅是假设,并不一定表示它是真正合法的HTML代码,如“<div></p>”。
第131行:否则,用正则quickExpr检测参数selector是否是稍微复杂一些的HTML代码(如“abc<div>”)或#id,匹配结果存放在数组match中。正则quickExpr的定义如下:
39 // A simple way to check for HTML strings or ID strings
40 // Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
41 quickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,
正则quickExpr包含两个分组,依次匹配HTML代码和id。如果匹配成功,则数组match的第一个元素为参数selector,第二个元素为匹配的HTML代码或undefined,第三个元素为匹配的id或undefined。下面的例子测试了正则quickExpr的功能:
quickExpr.exec( '#target' ); // ["#target", undefined, "target"]
quickExpr.exec( '<div>' ); // ["<div>", "<div>", undefined]
quickExpr.exec( 'abc<div>' ); // ["abc<div>", "<div>", undefined]
quickExpr.exec( 'abc<div>abc#id' ); // ["abc<div>abc#id", "<div>", undefined]
quickExpr.exec( 'div' ); // null
quickExpr.exec( '<div><img></div>' ); // ["<div><img></div>", "<div><img>
</div>", undefined]
第41行黑底白字的,在jQuery 1.6.3和之后的版本中,为了避免基于location.hash的XSS攻击,于是在quickExpr中增加了。在jQuery 1.6.3之前的版本中quickExpr的定义如下:
quickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,
在jQuery 1.6.3和之后的版本中,quickExpr匹配selector时如果遇到“#”,则认为不是HTML代码,而是#id,然后尝试调用document.getElementById()查找与之匹配的元素。而在jQuery 1.6.3之前的版本中,则只检查左尖括号和右尖括号,如果匹配则认为是HTML代码,并尝试创建DOM元素,这可能会导致恶意的XSS攻击。
假设有下面的场景:
在应用代码中出现$( location.hash ),即根据location.hash的值来执行不同的逻辑,而用户可以自行在浏览器地址栏中修改hash值为“#<img src=/ onerror=alert(1)>”,并重新打开这个页面;此时$( location.hash )在执行时变为$('#<img src=/ onerror=alert(1)>')。在jQuery 1.6.3之前,“#<img src=/ onerror=alert(1)>”被认为是HTML代码并创建img元素,因为属性src指向的图片地址并不存在,事件句柄onerror被执行并弹出1。这样一来,攻击者就可以在事件句柄onerror中编写恶意的JavaScript代码,例如,读取用户cookie、发起Ajax请求等。
读者可以访问以下地址,查看更多相关信息:
http://bugs.jquery.com/ticket/9521
http://ma.la/jquery_xss/
(1)参数selector是单独标签
如果参数selector是单独标签,则调用document.createElement()创建标签对应的DOM元素。相关代码如下所示:
134 // Verify a match, and that no context was specified for #id
135 if ( match && (match[1] || !context) ) {
136
137 // HANDLE: $(html) -> $(array)
138 if ( match[1] ) {
139 context = context instanceof jQuery ? context[0] : context;
140 doc = ( context ? context.ownerDocument || context : document );
141
142 // If a single string is passed in and it's a single tag
143 // just do a createElement and skip the rest
144 ret = rsingleTag.exec( selector );
145
146 if ( ret ) {
147 if ( jQuery.isPlainObject( context ) ) {
148 selector = [ document.createElement( ret[1] ) ];
149 jQuery.fn.attr.call( selector, context, true );
150
151 } else {
152 selector = [ doc.createElement( ret[1] ) ];
153 }
154
第135行:检测正则quickExpr匹配参数selector的结果,如果match[1]不是undefined,即参数selector是HTML代码,或者match[2]不是undefined,即参数selector是#id,并且未传入参数context。这行代码利用布尔表达式的计算顺序,省略了对match[2]的判断,完整的表达式如下:
if ( match && (match[1] || match[2] && !context) ) {
如果match不是null且match[1]是undefined,那么此时match[2]必然不是undefined,所以对match[2]的判断可以省略。
第138~140行:开始处理参数selector是HTML代码的情况,先修正context、doc,然后用正则rsingleTag检测HTML代码是否是单独标签,匹配结果存放在数组ret中。正则rsingleTag的定义如下:
50 // Match a standalone tag
51 rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/,
正则rsingleTag包含一个分组“(\w+)”,该分组中不包含左右尖括号、不能包含属性、可以自关闭或不关闭;“\1”指向匹配的第一个分组“(\w+)”。
第146~153行:如果数组ret不是null,则认为参数selector是单独标签,调用document.createElement()创建标签对应的DOM元素;如果参数context是普通对象,则调用jQuery方法.attr()并传入参数context,同时把参数context中的属性、事件设置到新创建的DOM元素上。
之所以把创建的DOM元素放入数组中,是为了在后面第160行方便地调用jQuery.merge()方法。方法jQuery.merge()用于合并两个数组的元素到第一个数组,相关内容在2.8.8节介绍和分析。
参数context的细节请参考2.1.1节;方法.attr()遇到特殊属性和事件类型属性时会执行同名的jQuery方法,相关内容将在8.2节介绍和分析;方法jQuery.isPlainObject()用于检测对象是否是“纯粹”的对象,即用对象直接量{}或new Object()创建的对象,这会在2.8.2节介绍和分析。
(2)参数selector是复杂HTML代码
如果参数selector是复杂HTML代码,则利用浏览器的innerHTML机制创建DOM元素。相关代码如下所示:
155 } else {
156 ret = jQuery.buildFragment( [ match[1] ], [ doc ] );
157 selector = ( ret.cacheable ? jQuery.clone(ret.fragment):ret.fragment ).childNodes;
158 }
159
160 return jQuery.merge( this, selector );
161
第156行:创建过程由方法jQuery.buildFragment()和jQuery.clean()实现,方法jQuery.buildFragment()返回值的格式为:
{
fragment: 含有转换后的 DOM 元素的文档片段
cacheable: HTML 代码是否满足缓存条件
}
第157行:如果HTML代码满足缓存条件,则在使用转换后的DOM元素时,必须先复制一份再使用,否则可以直接使用。
方法jQuery.buildFragment()和jQuery.clean()将分别在2.4节和第2.5节中介绍和分析。
第160行:将新创建的DOM元素数组合并到当前jQuery对象中并返回。
(3)参数selector是“#id”,且未指定参数context
如果参数selector是“#id”,且未指定参数context,则调用document.getElementById()查找含有指定id属性的DOM元素。相关代码如下所示:
162 // HANDLE: $("#id")
163 } else {
164 elem = document.getElementById( match[2] );
165
166 // Check parentNode to catch when Blackberry 4.6 returns
167 // nodes that are no longer in the document #6963
168 if ( elem && elem.parentNode ) {
169 // Handle the case where IE and Opera return items
170 // by name instead of ID
171 if ( elem.id !== match[2] ) {
172 return rootjQuery.find( selector );
173 }
174
175 // Otherwise, we inject the element directly into the jQuery object
176 this.length = 1;
177 this[0] = elem;
178 }
179
180 this.context = document;
181 this.selector = selector;
182 return this;
183 }
184
第162~164行:如果参数selector是“#id”且未指定参数context,则调用document.getElementById()查找含有指定id属性的DOM元素。
第166~168行:检查parentNode属性,因为Blackberry 4.6会返回已经不在文档中的DOM节点。
第169~173行:如果所找到元素的属性id值与传入的值不相等,则调用Sizzle查找并返回一个含有选中元素的新jQuery对象。即使是document.getElementById()这样核心的方法也需要考虑浏览器兼容问题,在IE 6、IE 7、某些版本的Opera中,可能会按属性name查找而不是id。例如,下面的HTML代码,通过document.getElementById()并不能找到正确的DOM元素:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="description" content="head meta description">
</head>
<body>
<div id="description">
body div description
</div>
<form name="divId">
<div id="divId"></div>
</form>
<script>
alert( document.getElementById( 'description' ).outerHTML );
alert( document.getElementById( 'divId' ).outerHTML );
</script>
</body>
</html>t
在IE7中的运行结果如图2-2和图2-3所示。
在这种情况下,Sizzle先通过document.getElementsByTagName("*")取出所有的DOM元素,然后检查每个元素的属性id是否与指定值相等,如果相等,则放入返回结果中。具体可查阅第3章关于“Sizzle”的介绍和分析。
第175~182行:如果所找到元素的属性id值与传入的值相等,则设置第一个元素、属性length、context、selector,并返回当前jQuery对象。
(4)参数selector是选择器表达式
相关代码如下所示:
185 // HANDLE: $(expr, $(...))
186 } else if ( !context || context.jquery ) {
187 return ( context || rootjQuery ).find( selector );
188
189 // HANDLE: $(expr, context)
190 // (which is just equivalent to: $(context).find(expr)
191 } else {
192 return this.constructor( context ).find( selector );
193 }
194
此时依然在字符串分支中,参数selector不是单独标签、复杂HTML代码、#id,而是选择器表达式。如果没有指定上下文,则执行rootjQuery.find( selector );如果指定了上下文,且上下文是jQuery对象,则执行context.find( selector );如果指定了上下文,但上下文不是jQuery对象,则执行this.constructor( context ).find( selector ),即先创建一个包含了context的jQuery对象,然后在该jQuery对象上调用方法.find()。
6.参数selector是函数
相关代码如下所示:
195 // HANDLE: $(function)
196 // Shortcut for document ready
197 } else if ( jQuery.isFunction( selector ) ) {
198 return rootjQuery.ready( selector );
199 }
200
第197~199行:如果参数selector是函数,则认为是绑定ready事件。从第198行代码可以看出$( function )是$( document ).ready( function )的简写。
方法jQuery.isFunction()将在2.8.2节介绍和分析。
7.?参数selector是jQuery对象
相关代码如下所示:
201 if ( selector.selector !== undefined ) {
202 this.selector = selector.selector;
203 this.context = selector.context;
204 }
205
第201~204行:如果参数selector含有属性selector,则认为它是jQuery对象,将会复制它的属性selector和context。而且在紧随其后的第206行会把参数selector中包含的选中元素引用,全部复制到当前jQuery对象中。
8.?参数selector是任意其他值
相关代码如下所示:
206 return jQuery.makeArray( selector, this );
207 },
第206行:如果selector是数组或伪数组(如jQuery对象),则都添加到当前jQuery对象中;如果selector是JavaScript对象,则作为第一个元素放入当前jQuery对象中;如果是其他类型的值,则作为第一个元素放入当前jQuery对象中。最后返回当前jQuery对象。
2.3.3 小结
至此,方法jQuery.fn.init( selector, context, rootjQuery )的12分支就介绍完了,相关的判断和执行过程可以整理为图2-4。