一.3-jQuery.prototype对象中的属性与方法(上)
源码96-280行
前言
上一篇说到了jQuery的extend方法,它是用来给jQuery函数扩展方法或者给jQuery的原型上加方法。
其实上,jQuery的原型上还是有一些属性和方法的。其中就有我们熟悉的init方法。
当我们调用 $("li") 时,new的便是jQuery.prototype.init构造函数
源码
首先源码比较长,我先写一个简版的
jQuery.fn = jQuery.prototype = {
jquery: core_version, // 49行 core_version = "2.0.3"
constructor: jQuery,
init: function(){},
selector: "",
length: 0,
toArray: function(){},
get: function(){},
pushStack: function(){},
each: function(){},
ready: function(){},
slice: function(){},
first: function(){},
last: function(){},
eq: function(){},
map: function(){},
end: function(){},
push: core_push,
// 其实这个方法就是Array.prototype.push。这样可以方便压缩代码。见53行及47行
sort: [].sort,
splice: [].splice
}可以看到还是有很多属性和方法的。有些实现比较简单,比如最后三个(push, sort,splice),有些很复杂,如init方法
1.jquery: core_version
获取jQuery的版本
console.log($().jquery); // "2.0.3"
有用处吗?
有,有些库是需要特定版本的jQuery的(有可能版本特别低),他的检测方法基本上就通过这种方法的。嘿嘿,只要你改一下这个,可能这个库就能用了。(我试过)
为啥子不换jQuery呢? 因为麻烦。哈哈(也不麻烦,jQuery有防冲突的机制,可以实现一个页面多个版本的jQuery库)
2.constructor: jQuery
重写下constructor的指向
3. init: function(){}
这个方法很重要,这个方法要需要处理的情况比较多。下面举些例子
$(function(){}); // 当dom加载完成后就调用此方法(我们自己写的代码都在这里面)
$("li"); // 选择所有的li标签
$("<div></div>"); // 创建元素
$("li", $(".class1")); // 选择所有的li标签,这个li标签必须在.class1标签下可以看到init方法中有大量的if判断(毕竟要完成大量的判断呢), 我们先整理下吧。写个简版吧。
jQuery源码中也给每一种的情况写了注释,在jQuery源码算是少见的注释了。
情况一
// 容错处理 $(""), $(null), $(undefined), $(false)
if (!selector) {
return this;
}
情况二
// 字符串的情况: 如选择器$("div"), $("div p") , 创建元素, $("<li></li>"),$("<ul><li></li></ul>")
if (typeof selector === "string"){
情况三
// $(this), $(document)
}else if(selector.nodeType){
情况四
// $(function(){})
}else if(jQuery.isFunction(selector){
}
if (){}情况一:
这是容错处理,就算用户乱输入,也尽量不报错
情况二:
情况二比较复杂,因为我们不但能选择元素,还能创建元素。
选择器可能是复杂选择器(如"div p"),也可能是简单选择器(如"div").
创建的元素可能是单个元素,也可能是多个元素。因此采取的策略都不太一样。
// 111-117if (selector.charAt(0) === "<" && selector.charAt(selector.length - 1) === ">" && selector.length >= 3){ // 创建元素 情况1
match = [null, selector, null];
}else{ // 选择器 情况2
match = rquickExpr.exec(selector);
}这个if判断还是很长的。首先这个字符串的第一个字符得是 <, 最后一个字符得是 >, 其次这个选择器的长度得大于等于3.
这么一看, 那便是创建 元素的情况。 $("<li></li>")和$("<li></li><li></li>")
有人可能会问了,假如我传入了这种的 $("<li>123")。jQuery会怎么处理?
这种情况肯定是走else了,放心,在else中会针对这种情况来进行处理的。
else语句中的rquickExpr是啥?
那是一个正则表达式(75行)
rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,
源码也有详细的解释
// A simple way to check for HTML strings // Prioritize #id over <tag> to avoid XSS via location.hash (#9521) // Strict HTML recognition (#11290: must start with <) 我来稍微说一个这个正则表达式吧。(如果看明白了这个正则表达式,也就知道了match的结果了) rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/ ^必须是什么什么开始 $ 必须以什么什么结束 (?:\s*(<[\w\W]+>)[^>]*|#([\w-]*)) 这是为啥子要包裹一下括号,而且还有?: 这是非捕获型分组,主要是提高性能用的。 \s*(<[\w\W]+>)[^>]*|#([\w-]*) \s (space)是空格的意思。\r\n\f\v\\x20 等都可以被认为是空格 *代表可以重复0次或多次。*可以联想到天上的星星。有时候没有,而有时候数都数的不过来 注意到中间有一个管道符,这代表或的意思。 情况一 (<[\w\W]+>)[^>]* 情况二 #([\w-]*)) 情况一 (<[\w\W]+>)[^>]* 注意分组内容 <[\w\W]+> 这个正则很有意思 必须以< 开头,中间的字符任意(包括空格), 个数大于等于1。 [^>]* 代表除了^之外,啥字符都能匹配的上,匹配次数是0次或者多次 情况二 #([\w-]*) 这是匹配id选择器用的 第一个字符得是 # [\w-]的字符范围比较广,0到9,a到z,A到Z以及字符-。
下面是情况1和情况2的总结
情况1总结
可能的情况: $("<li></li>") 和 $("<li></li><li></li>")
match的结果:
1. $("<li></li>")
[null, ‘<li></li>‘, null]
2. $("<li></li><li></li>")
[null, ‘<li></li><li></li>‘, null]情况2总结
可能的情况: $("#main") $("div p") $("<li>123")
match的结果:
rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/
1. $("#main")
["#main", undefined, "main"]
2. $("div p") // 对,没错,选择器其实不在这里做处理
null
3. $("<li>123")
[‘<li>123‘, ‘<li>‘, null]前面的代码只是做了预处理,分类了字符串的各种情况
// 120-174if (match && (match[1] || !context)){
// 创建标签或者id选择器的情况// id选择器的match的第二个元素为undefined,但是id选择器是没有context的。// context是啥? $(“li”, $("div")) $的第二个参数便是context,但我们如果使用id选择器的话,是不会传入第二个参数来缩小上下文的。// 因此id选择器也算一个 if (match[1]){ // 创建标签的情况,见下面 }else{ // id选择器的情况, 见下面 }
}else if (!context || context.jquery){
}else{
}创建标签的情况
1. 124行
// 124 context = context instanceof jQuery ? context[0] : context;
这个context便是我们在使用$ 时传入的第二个参数,用于缩小上下文。
$("li", $("div")); //此时context便是jQuery的实例,但是这玩意不能直接用的,因此要将jQuery转为dom。
// (因为jQuery实例是一种伪数组的形式,因此直接context[0])2. 127行-131行
jQuery.merge(this, jQuery.parseHTML(
match[1],
context && context.nodeType ? context.ownerDocument || context : document,
true
));这几行代码看起来比较复杂啊。
里面涉及了两个jQuery工具方法(通过上一节说的extend进行扩展的)
先说说这两个方法的使用吧
merge
var arr1 = [1,2];
var arr2 = [3,4];
$.merge(arr1, arr2);
console.log(arr1); // [1,2,3,4]
console.log(arr2); // [3,4]
var obj = {
0: 1,
1: 2,
length: 2
}
var arr3 = [3,4];
$.merge(obj, arr3);
console.log(obj); // {0:1, 1:2, 2:3,3:4, length: 4}
console.log(arr3); // [3,4]通过上面的代码可以看到merge工具方法接受两个参数(实现中也是两个形参)
这个两个参数可以是数组,也可以是伪数组(为什子要支持伪数组,因为jQuery实例便是一个伪数组)
第一个参数会融合第二个参数的元素。因此第一个参数会被修改,第二个参数就不会了。(可参考上面的例子)
parseHTML
这是一个处理html字符串的,如<li></li>, <li></li><li></li>
返回一个数组,数组中是创建好的单个标签。
$.parseHTML("<li></li>") // [li]
注意数组中的元素是node节点!!!!
$.parseHTML("<li></li><li></li>") // [li, li]
$.parseHTML("<ul><li></li><li></li></ul>") // [ul]$.parseHTML("<li></li><li></li><script>alert(1)</script>") // [li, li]$.parseHTML("<li></li><li></li><script>alert(1)</script>", document, true) // [li, li, script]parseHTML的第一个参数便是要解析的html字符串, 第二个参数是context,默认是document(大多情况是document,除非你想在iframe中操作)
第三个参数是一个布尔值。是否解析script标签,默认是不解析的。(为了安全考虑)
再来看看这几行代码
jQuery.merge(this, jQuery.parseHTML(
match[1],
context && context.nodeType ? context.ownerDocument || context : document,
true
));context的确定
context && context.nodeType ? context.ownerDocument || context : document
提示下: ownerDocument是对于iframe的处理。iframe.ownerDocument 返回这个iframe的document。而document就没有这个属性。
merge 会将 创建好的元素融合到this对象中, 举例子
$("<li></li>")这时this对象会长成这个样子
{
0: li
}第三个参数是true,这样script标签就能创建了
3.135-146行
这一部分是针对这样创建标签的
$("<p></p>", {title: 111, html: 123}}).appendTo($(document.body));// <p title="111">123</p>if (rsingleTag.test(match[1]) && jQuery.isPlainObject(context)) {
for (match in context) {
// Properties of context are called as methods if possible
if (jQuery.isFunction(this[match])) {
this[match](context[match]);
// ...and otherwise set as attributes
} else {
this.attr(match, context[match]);
}
}
}首先得满足这两个条件
要创建的标签得是单标签, 第二个参数得是对象字面量( {} 这个便是对象字面量)
说一下这个正则,涉及了反向引用
rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/ \1 是反向引用,这里的字符得和第一个分组的结果一样,即这部分 (\w+) 如果是这样的html字符 <li></ul>, 那么就不满足要求
然后for遍历这个对象,将属性和属性值放到这个创建好的this对象上
这里还有一个判断
if (jQuery.isFunction(this[match]))
这主要是针对我们使用html,text,css属性的,一但使用这些属性,就会调用jQuery的上的对应的方法
$("<li></li>", {html: 123});
// 会调用 this.html(123) ,注意这里this中的内容如果不是jQuery上的方法,就直接添加了
4. 148行
return this;
返回this, 里面的是已经创建好的dom对象
id选择器的情况(152-164)
id选择器就比较简单了,毕竟浏览器原生支持id选择器(虽然IE有bug)
// 调用原生方法
elem = document.getElementById(match[2]);
// Check parentNode to catch when Blackberry 4.6 returns
// nodes that are no longer in the document #6963
// 通过这个索引(#6963)可以去jQuery官网查看更详细的说明
// 这里也写了原因,是因为黑莓4.6删除了node后,这个node还会存在。因为需要判断下这个node的父节点在不在
if (elem && elem.parentNode) {
// Inject the element directly into the jQuery object
this.length = 1;
this[0] = elem;
}
this.context = document;
this.selector = selector;
return this;上面的一些属性(如selector, context)是不是比较熟悉呀
168行
else if (!context || context.jquery){
return (context || rootjQuery).find(selector);
}// rootjQuery = jQuery(document); 见867行针对这种情况
$(‘li‘) // context 不存在 $(document).find(‘li‘) $(‘li‘, $(‘div‘)) // $(‘div‘).find(‘li‘)
174行
对于复杂选择器的处理
return this.constructor(context).find(selector); // $(context).find(selector);
这个方法要调用就是要Sizzle选择器了(源码很复杂,特别是1.8.3版本后引入了编译,2000行代码)
好了,对于字符串处理的部分终于结束了,好长呀。
情况三
178行
如果传入了node节点
$(document.getElementById("main"))
else if(selector.nodeType){ this.context = this[0] = selector;
this.length = 1;
return this;}情况四
186行
如果传入了一个函数
else if (jQuery.isFunction(selector)) {
return rootjQuery.ready(selector);
}这个需要以后再说,这个形式是我们经常用的。
189-192行
如果写成这样子 $($("div")), 其实下面的代码就起作用了。
if (selector.selector !== undefined) {
this.selector = selector.selector;
this.context = selector.context;
}$("div")的selector肯定不是undefined,因此需要重新赋值下。因此 $($("div"))的效果与$("div")相同
194行
return jQuery.makeArray(selector, this)
这个makeArray工具方法如果只有一个参数的话,将会伪数组转为数组。
两个参数的话,会将融合另一个属性,并返回这个伪数组
jQuery.makeArray(selector, {0: 1, length: 1})
返回这样的对象
0: 1
1: "111"
length: 2