首页 > IT职业 > IT生活 > jQuery源码分析:深入理解js封装技术
jQuery实际上就是一个js函数库,对象jQuery作为window对象的一个属性封装了大量具有特定功能的函数。jQuery的代码很长,它是一个历史积累的过程,因此分析jQuery源代码就需要从其历史发展的过程来看,从简到繁,从少到多,去分析就容易得多了。
(function () {} )();
这段代码定义了一个匿名函数,并在js加载完毕之后执行一次。这样编写代码的目的就是使用函数作用域来隔离自己与他人的代码,防止污染到函数之外的代码。
js中的对象分为function、object两种。系统会自动为构造函数对象创建一个原型对象,并由由成员porototype指向,同时原型对象也有一个成员constructor指向构造对象。
js内置的对象有:Object、Array、Boolean、Number、String、Function、Argument、Math、Date、RegExp、Error。
(1)普通对象(object)
proto:指向构造自己的构造函数对象的原型对象
(2)构造对象(function)
proto:指向构造自己的构造函数对象的原型对象
prototype:指向自己的原型对象
(3)原型对象(object)
proto:指向构造自己的构造函数对象的原型对象
constructor:指向自己的构造函数对象
通过模拟类的特性,我们可实现继承和复用。实现继承的方式主要有一下4种:
构造继承
原型继承
实例继承
拷贝继承
下面展示的是构造继承,通过一个工厂类来实现
(function () {
// 定义一个工厂类 factory
// 工厂类构造函数
var factory = function () {}
// 私有成员定义,子类无法继承此成员
factory.wheels = 4;
// 公共成员定义,子类可以继承此成员
factory.prototype.add = function(){}
// 工厂方法创建一个对象
var o = new factory();
// o可以调用factory.prototype.add
// o.__proto__ -> factory.prototype
o.add();//o自身不存在add方法时系统会通过o.__proto__即原型链不断去查找,如果找不到就报错。
} )();
分析jQuery源码最基本结构,就是这么实现的
(function (window, undefined ) {
var
rootjQuery,
jQuery = function( selector, context ) {
return new jQuery.fn.init( selector, context, rootjQuery);
}
jQuery.fn = jQuery.prototype = {
constructor: jQuery,
init: function( selector, context, rootjQuery ) {
return this;
}
}
jQuery.fn.init.prototype = jQuery.fn;
/////////////////////////////////////////
// jQuery私有成员区开始
jQuery.merge = function () {}
// jQuery私有成员区结束
/////////////////////////////////////////
/////////////////////////////////////////
// jQuery公共成员区开始
jQuery.fn.merge = function () {}
// jQuery公共成员区结束
/////////////////////////////////////////
window.jQuery = window.$ = jQuery;
})(window);
首先,定义了构造函数jQuery
var jQuery = function ( selector, context ) {}
这与下面的实现方式是一样的:
function jQuery ( selector, context ) {}
参数selector为选择字符串(类似于CSS选择器),context用于保存上下文环境。
紧接着:
window.jQuery = window.$ = jQuery;
这段代码用于将jQuery注册为window下的成员,即全局成员,同时用$替换jQuery,所以就有了我们经常使用的$("div")就可以直接返回一个jQuery对象,显得更为方便简洁。
每次调用$()都会返回一个新的jQuery对象,避免使用同一个对象产生干扰和修改。它采用的思路是:
return new jQuery.fn.init( selector, context, rootjQuery);
每次调用$(),都会new一个jQuery.fn.init对象,这里解释一下jQuery.fn,从后面代码
jQuery.fn = jQuery.prototype = {}
可以看出,jQuery就是和jQuery.prototype一样对同一个原型对象的引用,定义为fn也是function的简写,一方面是为了书写方便,另一方面我们从命名可以看出fn主要是用来为jQuery的原型对象增加公共方法,用于子对象的继承。
作者为jQuery.prototype新创建了一个自定义对象,因此在匿名对象内部需要增加这部分代码,确保原型对象指回jQuery这个构造对象本身,确保完整。:
constructor: jQuery
通过jQuery.fn.init构造出一个对象,然后将其引用返回,也就是说,我们通过$()返回的是指向jQuery.fn.init构造的对象的一个引用。那么这个对象的proto应该就是指向jQuery.fn.init.prototype,也就是说
$().__proto__ = jQuery.fn.init.prototype;
我们都知道,jQuery有个连缀操作,也就是说可以下面代码这样一直写下去:
$("div").eq(0).css("color","red");
但是$().proto 指向了 jQuery.fn.init.prototype,要想实现连缀操作,我们就必须让jQuery.fn.init.prototype指向jQuery.prototype,因此才有了后面这段代码:
jQuery.fn.init.prototype = jQuery.fn;
这样一来的话,通过$()构造出来的对象,其原型对象就是jQuery的原型对象,即:
$().__proto__ = jQuery.prototype;
$()就可以连缀操作jQuery的公共方法。
之前我们分析了jQuery源码的基本结构,了解了如何为jQuery添加公共方法和私有方法。
//私有方法定义 jQuery.merge = function () {}
//公共方法定义 jQuery.fn.merge = function () {}
(1)实现自己的添加函数
jQuery.extend = function () {
var
copy,
options = arguments[0] || {};
if ( typeof options === "object" && typeof options === "function" ) {
for ( key in options) {
copy = options[key];
if ( this === copy ) { continue; }
if ( copy !== undefined ) {
this[key] = copy;
}
}
}
return this;//实现连缀操作 }
jQuery.fn.extend = function () {
var
copy,
key,
options = arguments[0] || {};
if ( typeof options === "object" && typeof options === "function" ) {
for ( key in options) {
copy = options[key];
if ( this === copy ) { continue; }
if ( copy !== undefined ) {
this[key] = copy;
}
}
}
return this;//实现连缀操作 }
jQuery.extend 和 jQuery.fn.extend 实现的代码完全一致,现在考虑一下是否可以将其合并。
当我们调用jQuery.extend的时候,this是jQuery,即为jQuery进行扩展;当调用jQuery.fn.extend的时候,this是jQuery.fn,即为jQuery.fn(jQuery.prototype进行扩展)。因此,我们可以将其进行合并,即:
`
jQuery.extend = jQuery.fn.extend = function () {
var
copy,
key,
options = arguments[0] || {};
if ( typeof options === "object" && typeof options === "function" ) {
for ( key in options ) {
copy = options[key];
if ( this === copy ) { continue; }
if ( copy !== undefined ) {
this[key] = copy;
}
}
}
return this;
}
(2)实现对象的合并
obj1 = {id:1,address:"China",owner:{age:30}}
obj2={age:25,owner:{name:"liebert"}}
objcombine = {id:1,address:"China",age:25,owner:{name:"liebert",age:30}}
循环遍历被合并对象成员,设每次循环得到的成员为copy,同时取出接收合并对象的相同key的成员,记为src。对象obj1的成员不能指向对象A本身,因此,首先检测待合并对象成员
if ( obj1 === copy ) { continue; }
如果对象合并采取浅拷贝的方式,直接进行赋值操作即可
obj1[key] = copy;
如果对象合并采取深拷贝的方式,需要进行递归拷贝,不是所有的变量都需要进行深拷贝,普通的变量是不需要进行深拷贝的,也就是存储在栈空间的变量。深拷贝针对的是object
jQuery.extend = function () { var
key,
src,
copy,
isDeep,
copyIsArray,
i = 1,
length = arguments.length;
target = arguments[0] || {}; if ( typeof target === "boolean" ) { //isDeep参数存在,并获取
isDeep = target; //如果isDeep参数存在,需要调整接收合并对象为第二个参数
target = arguments[1] || {}; //待合并的对象为第三个参数,即索引为3-1=2
i = 2;
} for ( ; i (3)对比jQuery中extend的实现
为了方便jQuery方法的添加,jQuery定义了jQuery.extend和jQuery.fn.extend两个方法。
jQuery.extend = jQuery.fn.extend = function () { var
//对待合并对象的临时引用
options,
//键名
key, //接收拷贝的变量
src, //待拷贝的变量
copy, //接收拷贝的临时变量
clone, //待复制的变量是否为数组
copyIsArray, //默认为将取第i+1=2个参数
i = 1,
isDeep = false,
target = arguments[0] || {},
length = arguments.length; // 处理深拷贝情况
if ( typeof target === "boolean" ) {
isDeep = target;
target = arguments[1] || {};
i = 2;
} //处理字符串情况
if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
target = {};
} //如果只有一个参数,就是扩展jQuery自己,修改索引i = 0,target指向jQuery或者jQuery.fn
//当调用jQuery.extend时,this为jQuery;当调用jQuery.fn.extend时,this为jQuery.fn
if ( length === i) {
target = this;
--i;
} for ( ; i 先对函数的入口参数进行说明:
jQuery.extend( [isDeep], target[,object1,..., objectN] )
isDeep:用于指定是否进行深拷贝,是一个可选参数,默认为false,即不进行深拷贝
target:接收拷贝对象,如果不指定object1以及后续的对象,则是对jQuery本身进行扩展
object1:待合并到target中的对象
再来谈一谈具体的处理思路:
首先,对参数的第一个入口参数arguments[0]进行判断,如果为布尔类型就说明指定了isDeep这个参数;
var target = arguments[0] || {};
if ( typeof target === "boolean" ) {
//获取参数中的isDeep值
isDeep = target;
//获取接收拷贝的对象引用
target = arguments[1] || {};
//设定拷贝的起始索引,从函数的第三参数开始,跳过isDeep和target
i = 2;
}
其次,检测target对象类型,如果不是object或function,则需要创建一个新的对象用于接收
if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
target = {};
}
再者,检测一个参数存在的情况,如果是,则视为对jQuery或jQuery.fn本身进行扩展
if ( length === i ) {
target = this,
--i;
}
接着,开始进行对象的合并。待合并的对象总共有length个,因此需要循环length-i次
for ( ; i最后,返回合并后的对象
return target;4.实现CSS选择器功能
js为我们提供了如下三个函数,用于实现DOM节点的选择:
getElementById();
getElementsByClassName();
getElementsByTagName();但是这三个函数的名字太长了,不容易记忆,使用过程中也容易出错,更重要的是不能够向CSS选择器那样,通过一连串的选择符实现节点的选择,因此我们想到通过js现有的函数去模拟CSS选择器。
function ( window ) { var rquickExpr = /^(?:[^#)[^>]*$|#([w-]*)$)/, jQuery = function ( selector ) { return new jQuery.fn.init( selector ); } ///////////////////////////////////////////////////// // jQuery private defination // jQuery私有成员区开始 jQuery.type = function ( obj ) { return obj == null ? String(obj) : obj.toString(); } jQuery.isWindow = function () { } jQuery.merge = function ( first, second ) { var l = second.length, i = first.length, j = 0; if ( typeof l == "number" ) { for (;j html" if ( selector.charAt(0) ==="" && selector.length >=3 ) { match = [null, selector, null]; } else { match = rquickExpr.exec( selector ); } } // 处理匹配了html标签 if ( match && (match[1] || !context) ) { if( match[1] ) { if ( match[1] ) { context = context instanceof jQuery ? context[0] : context; doc = ( context && context.nodeType ? context.ownerDocument || context : document ); } } else { elem = document.getElementById( match[2] ); if ( elem && elem.parentNode ) { //return rootjQuery.find( selector ); this.length = 1; this[0] = elem; } this.context = document; this.selector = selector; return this; } } else if ( !context || context.jquery) { return ( context || rootjQuery ).find( selector ); } else { return this.constructor( context ).find( selector ); } return jQuery.makeArray( selector, this ); }, eq: function( i ){ i = +i; return i === -1 ? this.slice( i ) : this.slice( i, i+1 ); }, slice: function() { return this.pushStack(Array.prototype.slice.apply( this, arguments ), "slice", Array.prototype.slice.call(arguments).join(",")); }, pushStack: function( elems, name, selector ) { // Build a new jQuery matched element set var ret = jQuery.merge( this.constructor(), elems ); 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 of jQuery.prototype // jQuery公共成员区结束 ///////////////////////////////////////////////////// jQuery.fn.init.prototype = jQuery.fn; //Expose jQuery to the global window.jQuery = window.$ = jQuery; } )(window);5.each函数分析
(1)apply和call区别
// obj:这个对象将替代Function类里的this对象// args:必须是数组变量,只能接受数组格式的参数Function.apply( obj, args )call对参数没有要求
(2)each函数参数分析
obj:待遍历的对象列表
callback:回调函数,由obj列表中的对象进行调用
args:可选的回调函数参数(3)each函数功能分析
each函数用于遍历对象列表中对象,每一次遍历都执行一个过程(也称为函数),这个过程作用于当前遍历到的对象,也就是说这个过程中的this为当前对象。
jQuery.extend({ each: function ( obj, callback, args ) { var name, i = 0, length = obj.length, isObj = length === undefined; if ( args ) { if ( isObj ) { for ( key in obj ) { if ( callback.apply( obj[key], args) === false ) { break; } } } else { for ( ; i6.parseXML函数分析
(1)功能分析
parseXML函数主要用于对XML文件进行解析,因为IE和其他浏览器对XML文件提供了不同方法进行解析,jQuery对XML文件进行了封装,兼容各个浏览器差异性,对用户提供统一的操作接口。
(2)代码分析
jQuery.extend( { error: function ( msg ) { throw new Error( msg );//报错 }, parseXML: function ( data ) { var domParser, xml; // 首先对参数进行校验,必须为字符串类型 if ( !data || typeof data !== "string" ) { return null; } try { //检测浏览器window对象是否提供DOM节点解析对象,IE是不提供的,针对不同浏览器提供的方法对XML字符串进行解析,生成XML对象 if ( window.DOMParser ) { domParser = new DOMParser(); xml = domParser.parseFromString( data, "text/xml" ); } else { xml = new ActiveXObject( "Microsoft.XMLDOM" ); xml.async = "false"; xml.loadXML( data ); } } catch (e) { xml = undefined; } //检测xml合法性,报告错误 if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { jQuery.error( "Invalid XML: " + data ); } return xml; } } );8.find函数分析
jQuery.find = Sizzle;function Sizzle( selector, context, results,seed ) { //参数过滤 results = results || []; context = context || document; var nodeType = context.nodeType; if ( !selector || typeof selector !== "string" ) { return results; } if ( nodeType != 1 && nodeType != 9 ) { return []; } }9.data函数
jQuery.fn.extend({ data: function ( key, value ) { var elem = this[0]; //获取全部值 if ( key === undefined ) { if ( this.length ) { data = jQuery.data( elem ); if ( elem.nodeType === 1 && !jQuery._data(elem, "parsedAttrs") ) { attr = elem.attributes; for ( ; i10.词法分析器
词法分析匹配正则对象集合
whitespace = "[\x20\t\r\n\f]"; characterEncoding = "(?:\\.|[-\w]|[^\x00-\xa0])+"; identifier = characterEncoding.replace( "w", "w#" ); attributes = "\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + "*(?:" + operators + whitespace + "*(?:(['"])((?:\\.|[^\\])*?)\3|(" + identifier + ")|)|)" + whitespace + "*\]"; matchExpr = { "ID": new RegExp( "^#(" + characterEncoding + ")" ), "CLASS": new RegExp( "^\.(" + characterEncoding + ")" ), "NAME": new RegExp( "^\[name=['"]?(" + characterEncoding + ")['"]?\]" ), "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), "ATTR": new RegExp( "^" + attributes ), "PSEUDO": new RegExp( "^" + pseudos ), "POS": new RegExp( pos, "i" ), "CHILD": new RegExp( "^:(only|nth|first|last)-child(?:\(" + whitespace + "*(even|odd|(([+-]|)(\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + "*(\d+)|))" + whitespace + "*\)|)", "i" ), // For use in libraries implementing .is() "needsContext": new RegExp( "^" + whitespace + "*[>+~]|" + pos, "i" ) }Token格式 { value:'匹配到的字符串', type:'对应的Token类型', matches:'正则匹配到的一个结构'}function tokenize( selector, parseOnly ) { // soFar:目前为止还未分析的字符串 // groups:目前已经匹配到的规则组 var matched, match, tokens, type, soFar, groups, preFilters, cached = tokenCache[ expando ][ selector + " " ]; if ( cached ) { return parseOnly ? 0 : cached.slice( 0 ); } soFar = selector; groups = []; preFilters = Expr.preFilter; while ( soFar ) { // Comma and first run if ( !matched || (match = rcomma.exec( soFar )) ) { if ( match ) { // Don't consume trailing commas as valid soFar = soFar.slice( match[0].length ) || soFar; } groups.push( tokens = [] ); } matched = false; // Combinators // 关系选择符 if ( (match = rcombinators.exec( soFar )) ) { tokens.push( matched = new Token( match.shift() ) ); soFar = soFar.slice( matched.length ); // Cast descendant combinators to space matched.type = match[0].replace( rtrim, " " ); } // Filters // 正则过滤 for ( type in Expr.filter ) { if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || (match = preFilters[ type ]( match ))) ) { tokens.push( matched = new Token( match.shift() ) ); soFar = soFar.slice( matched.length ); matched.type = type; matched.matches = match; } } if ( !matched ) { break; } } // Return the length of the invalid excess // if we're just parsing // Otherwise, throw an error or return tokens return parseOnly ? soFar.length : soFar ? Sizzle.error( selector ) : // Cache the tokens tokenCache( selector, groups ).slice( 0 ); }11.select
function select( selector, context, results, seed, xml ) { var i, tokens, token, type, find, // 解析词法格式 match = tokenize( selector ), j = match.length; if ( !seed ) { // Try to minimize operations if there is only one group // 没有多分组,只是个单选则器,也就是selector中没有逗号,可以进行一些优化操作 if ( match.length === 1 ) { // Take a shortcut and set the context if the root selector is an ID // 取出Token序列 tokens = match[0] = match[0].slice( 0 ); // 如果选择器selector第一部分为ID选择器 if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && context.nodeType === 9 && !xml && Expr.relative[ tokens[1].type ] ) { context = Expr.find["ID"]( token.matches[0].replace( rbackslash, "" ), context, xml )[0]; // 如果查询结果为空(selector第一个ID选择器查询为空),就不用再查找了 if ( !context ) { return results; } // 去掉第一个ID选择器 selector = selector.slice( tokens.shift().length ); } // Fetch a seed set for right-to-left matching // 没有结构性伪类的话,从右至左进行扫描tokens中选择器序列(从数组后往前)匹配 for ( i = matchExpr["POS"].test( selector ) ? -1 : tokens.length - 1; i >= 0; i-- ) { // 获取一个规则 token = tokens[i]; // Abort if we hit a combinator // 遇到关系选择符( > + ~ 空 )终止循环 if ( Expr.relative[ (type = token.type) ] ) { break; } // 如果搜索器中支持此种类型查找 if ( (find = Expr.find[ type ]) ) { // Search, expanding context for leading sibling combinators if ( (seed = find( token.matches[0].replace( rbackslash, "" ), rsibling.test( tokens[0].type ) && context.parentNode || context, xml )) ) { // If seed is empty or no tokens remain, we can return early tokens.splice( i, 1 ); selector = seed.length && tokens.join(""); if ( !selector ) { push.apply( results, slice.call( seed, 0 ) ); return results; } break; }//end if ( (seed = find }// end if ( (find = Expr.find[ type ]) ) }//end for ( i = matchExpr["POS"].test( selector ) } } // Compile and execute a filtering function // Provide `match` to avoid retokenization if we modified the selector above compile( selector, match )( seed, context, xml, results, rsibling.test( selector ) ); return results; }关于jQuery的源码暂时分析到这里,后面还会继续分析。
作者:李伯特
链接:https://www.jianshu.com/p/2c6056449b3d
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/2650/viewspace-2805255/ ,如需转载,请注明出处,否则将追究法律责任。