create: 2013-05-05 22:45
update: 2013-05-10 14:00
因为前面啰嗦的比较多,再加上 core.js 剩下的内容也不少,于是再续写一篇。
在这篇中,将要介绍 jQuery 中许多重要的静态方法。所谓的静态方法,就是指直接挂接到 jQuery 对象上的方法,不需要进行实例化即可使用。
继承
jQuery 使用的继承方式很直接,一句话:复制。(说“俩字”可能更准确:)
所有的重担都压到了 extend 方法上。
jQuery.extend = jQuery.fn.extend = function() {
... ...
这句话表明 extend 即是静态方法,同时也是 jQuery 原型上的方法。
extend 方法并没有显式的列出参数列表。不过官网的 API 上已经标明了:
jQuery.extend( target [, object1 ] [, objectN ] )
jQuery.extend( [deep ], target, object1 [, objectN ] )
这两个列表的区别在于第一个参数, deep 表示深拷贝,没有它的话就是普通的拷贝,将 object1 以及后续的对象上的属性和方法复制到 target——目标对象上。
什么叫做深拷贝?举个例子:
var obj1 = {
arr : [ 1, 2, 3 ],
innerObj : {
name : 'innerObj'
}
};
var obj2 = {}; // 我们定义了一个对象
for( var key in obj1 ) { //开始复制
obj2[key] = obj1[key];
}
以上就是一个简单的拷贝过程,通过 for 循环将 obj1 的属性复制到 obj2 上。
这是深拷贝吗?不是,这叫浅拷贝。原因在于,obj1 中的 arr 和 innerObj 为数组或对象,我在复制的时候,只是将它们的引用复制到了 obj2 上,现在的情况就是,复制完毕了,obj1 和 obj2 的 arr 与 innerObj 实际上都是同一个数组和对象。我们可以验证下:
obj1.arr.push( 4 );
console.log( obj2.arr );
//更直观的方式
console.log( obj1.arr === obj2.arr ) //注意这里用的是 ===
要实现深拷贝,就需要判断出拷贝的属性是否是数组或对象,如果是的话,继续循环这些数组或对象,直到碰到基本类型为止(我忽略了函数,虽然它们也是对象,但通常函数上不会包含什么需要拷贝的属性)。
终于可以看源码了。
var options, name, src, copy, copyIsArray, clone,
target = arguments[0] || {},
i = 1,
length = arguments.length,
deep = false;
// 处理深拷贝情况
if ( typeof target === "boolean" ) {
deep = target;
target = arguments[1] || {};
// 跳过布尔值与目标对象
i = 2;
}
参数声明没什么好说的,下面的 if 判断了 target 是否为布尔值,这就是对应前面参数列表中的第二种,即深拷贝模式。
// 处理当 target 为字符串或其他内容的情况
if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
target = {};
}
// 如果只传入一个参数,那么扩展 jQuery 本身
if ( length === i ) {
target = this;
--i;
}
当 target 既不是对象也不是函数时,就强制把它设置为一个空对象。第二个判断上的注释写的很明白了,不赘述。
for ( ; i < length; i++ ) {
// 只处理非空或不是 undefined 的值
if ( (options = arguments[ i ]) != null ) {
// 扩展基础对象
for ( name in options ) {
src = target[ name ];
copy = options[ name ];
// 防止无限循环
if ( target === copy ) {
continue;
}
// 如果是在合并普通对象或者数组,递归执行
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 );
// 不要设置未定义的值
} else if ( copy !== undefined ) {
target[ name ] = copy;
}
}
}
}
// 返回修改后的数组
return target;
extend 方法允许传入多个值,这个 for 循环就是为了处理这种情况。
第一个 if 判断已经将 null 和 undefined 参数清除出去了。这里一个值得学习的技巧是括号内的赋值。
if ( (options = arguments[ i ]) != null )
将 arguments[ i ] 赋值给 options,这样我们在下方就只需要使用局部变量 options,而不必使用 arguments[ i ]。而且赋值表达式返回的结果就是它的右值——即等号右边的值,这样我们就在赋值的同时,通过和 null 进行比较完成了非空的判断,实在是一举两得。
// 防止无限循环
if ( target === copy ) {
continue;
}
这里需要说明下,防止无限循环?为什么会产生无限循环?还是举个香喷喷的栗子吧:
var o = {};
o.obj = o;
var i = 0;
function extend(target, o) {
for(var key in o) {
var copy = o[key];
//if(copy === target) continue;
if(typeof copy == 'object') {
i++
if(i >= 20) break;
console.log('recursive');
console.log(target)
target[key] = extend({}, copy);
}
}
return target;
}
extend(o, o);
这里的 extend 是我自己写的方法,与 jQuery 中类似,去掉了无关紧要的内容,最主要的是,去掉了 copy 与 target 相等的比较。运行下瞧瞧。
结果肯定是无限循环,为了不让浏览器崩溃,我就设定了循环20次后退出。
对象 o 中的属性 obj 是执行自身的引用。我用 o 来扩展它本身,究竟发生了什么事情?
当 for 循环启动后,此时的 key 应该是 'obj',那么 copy 便等于 o.obj,就是 target 本身。在经过 typeof 测试后,认定 copy 是一个对象,于是顺利进入内层的 extend,此时的 extend({}, copy),便回到了本段开始的位置,仿佛踏上了那艘恐怖游轮……
因此,在处理这种拷贝的时候,需要留心无限循环。
一个小问题:有哪个常见对象是拥有自身引用的?答案在下边找。
介绍完 extend 方法,源码中立刻就用了 jQuery.extend({ ... }),通过前面说的内容,你应该知道,这是在扩展 jQuery 对象自身呐。
循环
这段只介绍一个方法,但却极其常用,那就是 $.each()。
// 参数 args 只供内部使用
each: function( obj, callback, args ) {
var value,
i = 0,
length = obj.length,
isArray = isArraylike( obj ); // 类数组。暂且理解为和数组行为很像。
if ( args ) {
if ( isArray ) {
for ( ; i < length; i++ ) {
value = callback.apply( obj[ i ], args );
if ( value === false ) {
break;
}
}
} else {
for ( i in obj ) {
value = callback.apply( obj[ i ], args );
if ( value === false ) {
break;
}
}
}
// 一个特别的、快速的版本,适用于使用 each 的众多常见场景
} else {
if ( isArray ) {
for ( ; i < length; i++ ) {
value = callback.call( obj[ i ], i, obj[ i ] );
if ( value === false ) {
break;
}
}
} else {
for ( i in obj ) {
value = callback.call( obj[ i ], i, obj[ i ] );
if ( value === false ) {
break;
}
}
}
}
return obj;
},
由于一个内部的参数 args 进来掺乎,导致整个方法内部出现了一个大的分支,好在上下两端内容及其相似,我们还是来关注下面那部分。
each 如何使用?简单来说,它就是代替了你手动书写如下内容:
for(int i = 0; i < len; i++ ) {
...
}
该 each 方法就是把要循环的对象或数组,与需要在对象或数组元素上执行操作的函数(就是 callback)合并在了一起,源码中值得说的,就是在该函数的开头,用变量 length 保存了 obj 的长度,一旦发现 obj 原来是个数组,那么就使用这个 length 变量来进行操作。循环数组前缓存数组长度,是一个很常见的优化方式,也是良好的编程习惯:)
至于源码中的其他内容,没有什么可说的,如果你要是不知道 call 的含义……那么还是去别的地方先了解一下吧。
类型判断
JS 是弱类型语言,声明写个 var 了事,一个变量上一句还是个对象,下一句可能就变数字了,虽然这么做不是好的编程习惯,但人家语言也没限制咱不是?总之一句话,写 JS 少不了要判断变量类型。
很早之前,那时候我还不知道在干啥,判断 JS 类型就是用 typeof 关键字,它在判断对象和基本类型上还比较有用,但你要塞给它一个对象和一个数组,它就搞不明白了。除了这个关键字,还有 instanceOf,这是用来判断实例和构造函数之间是否有关联的,用来判断 Array 这种正合适,但可惜了,当页面里有 iframe 的时候,它的 Array 和父页面里的 Array 就是两码事。这些陈年往事我就不说了,毕竟没怎么经历过,还是看 kangax 这篇博文更明白些:'instanceof' considered harmful (or how to write a robust 'isArray')
文章中介绍了一种有效、稳定的判断变量类型的方式 —— Object.prototype.toString。比如说我要检测一个变量 args,我便如此使用:
var returnString = Object.prototype.toString.call( args );
这个方法检测变量 args 的内部属性 [[Class]],然后返回一个形如 '[object Array]' 的字符串,注意,前面的'[object '与最后的']'是固定不变的,变化的只是后面的 'Array',如果 args 确实是一个数组,那么就会返回这个字符串,但如果它是一个函数,则会返回 '[object Function]'。
其实这时候不用看源码,你就应该知道如何去写判断类型的函数了,但是跟着 jQuery 还是会学到一些技巧。
由于前面所说,toString 函数返回的字符串中,前一部分和最后的']'是不变的,这样的话,如果我们在每个类型判断的函数中都将整个字符串写全,那重复的内容就显得稍微多了些。看 jQuery 是怎么做的。
jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
class2type[ "[object " + name + "]" ] = name.toLowerCase();
});
class2type 是在 core.js 文件开头定义的变量。这里使用了 each 方法,用空格分隔的字符串表示的是类型名称,使用 split 切割成数组后,循环将类型名称赋予到 class2type 变量上。
type: function( obj ) {
if ( obj == null ) {
return String( obj );
}
// 支持: Safari <= 5.1 (函数式的 RegExp)
return typeof obj === "object" || typeof obj === "function" ?
class2type[ core_toString.call(obj) ] || "object" :
typeof obj;
}
这个 type 方法就是 jQuery 中判断参数类型的。第一个分支,obj == null,这里就可以将 undefined 与 null 过滤出来,这两种类型比较常见,判断也简单,你可以试试 String( obj ) 会返回什么内容。
最后一句可能看起来比较麻烦,多种运算符混在了一起。这个得按照它们的优先级来判断哪个先执行,有关这方面知识,可以查看这里。当你熟悉这些知识后,很快就可以将它们分组了,最后的情形是这样的:
( typeof obj === "object" || typeof obj === "function" ) ?
( class2type[ core_toString.call(obj) ] || "object" ) :
typeof obj
这里如此判断的原因有二:一,性能,使用 typeof 要比 toString 方法效率更高。来看一个 jsPerf 上的示例,typeof 对于处理非对象类型十分有效,此时就没必要拿出 toString 这把武器了;而第二,就是注释中写的有关 Safari 的问题,在 5.1 版本前,它会错误的将正则表达式认为是函数,其实早期的 chrome 也会有这问题。总之,当发现 obj 是个对象或者函数,那么就使用 toString 的方式来得到准确类型。
isFunction: function( obj ) {
return jQuery.type(obj) === "function";
},
isArray: Array.isArray || function( obj ) {
return jQuery.type(obj) === "array";
},
isWindow: function( obj ) {
return obj != null && obj == obj.window;
},
isNumeric: function( obj ) {
return !isNaN( parseFloat(obj) ) && isFinite( obj );
},
isEmptyObject: function( obj ) {
var name;
for ( name in obj ) {
return false;
}
return true;
},
isPlainObject: function( obj ) {
// 必须是一个对象。
// 由于 IE 的存在,我们还必须检查 constructor 属性是否存在。
// 确保 DOM 节点与 window 对象不能通过测试
if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
return false;
}
try {
// constructor 属性非自身属性的是 Object
if ( obj.constructor &&
!core_hasOwn.call(obj, "constructor") &&
!core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
return false;
}
} catch ( e ) {
// 在测试特定的宿主对象时,IE8 与 9 会抛出异常 #9897
return false;
}
// 自身属性会先被遍历,如果最后一个属性属于自身,那么所有属性都属于对象本身
var key;
for ( key in obj ) {}
return key === undefined || core_hasOwn.call( obj, key );
}
上面是 jQuery 对一些更具体类型的判断。isFunction 使用的就是上面介绍过的 type 方法。ES 5中新增了判断 Array 的方法:Array.isArray,但出于兼容性考虑,在浏览器不支持该方法的情况下,会转而使用 type 方法。
isWindow 判断了 window 对象,判断的前一部分不用提,后一部分正好解答了我前面提出的小问题,即 window 对象拥有一个名为 window 的属性,该属性指向了自身。但这里判断使用了 ==,在最新的 jQuery 版本中这里修改为了 ===,应该会更精确了。
isNumeric 用于判断数字,按理说,typeof num === 'number' 即可,但通常我们想让变量是一个有意义的数字,而 NaN、Infinity 基本不会使用,但它们在执行了 typeof 后也会返回 'number',因此此处使用了两个方法:isNaN 和 isFinite 来将这两种情况判断出来。
isEmptyObject 就是使用 for 来对变量进行循环,如果不能循环,自然就说明变量不包含任何内容,是个空对象。但不知为何,这里并没有对 obj 是否为对象做判断,如果传入一个数字,一个空字符串,它也会返回 true。我去官网查了文档,在该方法的描述中明确提了出来,即传入的 obj 必须是一个普通的 JavaScript 对象,我前面传入的明显不符合要求,但什么叫做 "普通的 JavaScript 对象" 呢?下面的 isPlainObject 方法就做了判断。
根据文档定义,所谓的 'plain Object' 即通过 '{}' 或 'new Object' 创建,而内部属性[[Class]]不是 'Object'、window、DOM 节点这些都不是普通对象。函数中的第一个分支已经将大部分不符合要求的类型排除了出去,但有些宿主对象会被漏掉,比如说 window.location,在 Chrome 和 Firefox 的最新版本下,对它调用 Object.prototype.toString 会返回 '[object Location]',那么使用 jQuery.type() 会返回 'object',可它却不符合普通对象的定义,因此需要通过 try catch 中的代码来进一步判断,加 try catch 的原因在注释中写明了。至于最后那段 for 循环,在新版代码中已经去掉,直接返回 true 了。
数组方法
上一篇中,我们介绍了数组对象,也介绍了它的一些方法,但是在 core 文件的后半部分,jQuery 为自己扩展了更多更有用的数组方法。其中 each 我们已经在上面讲完了,来看看剩下的内容。
merge: function( first, second ) {
var l = second.length,
i = first.length,
j = 0;
if ( typeof l === "number" ) {
for ( ; j < l; j++ ) {
first[ i++ ] = second[ j ];
}
} else {
while ( second[j] !== undefined ) {
first[ i++ ] = second[ j++ ];
}
}
first.length = i;
return first;
}
merge 方法很常用,所以提前讲。merge 在中文的意思是"合并",该方法的作用就是将两个参数的内容合并在一起并返回,但需要注意的是,它实际上是将第二个参数的内容追加到第一个参数上,再将修改后的第一个参数返回。
<del>看着代码你可能觉得很奇怪,明明就是将两个数组合并,只要循环第二个数组即可,为什么这里还需要判断 second 的 length 属性是否为数字呢?我也觉得奇怪,这里也没有写注释,但代码不会无缘无故的写成这样,只能追本溯源,看看早期版本的源码了。</del>
你可以使用 merge 方法来备份一个数组,例如:
var newArray = $.merge( [], oldArray );
这样 newArray 就会拥有和 oldArray 一样的内容。
function isArraylike( obj ) {
var length = obj.length,
type = jQuery.type( obj );
if ( jQuery.isWindow( obj ) ) {
return false;
}
if ( obj.nodeType === 1 && length ) {
return true;
}
return type === "array" || type !== "function" &&
( length === 0 ||
typeof length === "number" && length > 0 && ( length - 1 ) in obj );
}
isArrayLike 并非是 jQuery 上的静态方法,它定义于 core 文件的最下方,因为其他数组方法中使用了它,所以提前讲解。
看这个名字,它会判断传入的 obj 是否类似数组。根据上一篇的讲解,拥有 length 属性的很可能就是数组了。源码中首先用 isWindow 方法将 window 对象排除,因为 window 对象也有 length 属性,这个属性表明在当前窗口中拥有框架(frame 或 iframe 元素) 的个数。
第二个判断中,nodeType 为 1 表示 obj 为元素节点,如果它恰好拥有 length 属性,那么就认为它和数组类似。(我没想到例子)
最后的返回值中判断了不少内容,首先 type 为 'array' 的肯定就是数组,其次是非函数,但必须拥有 length,且 length 必须是数字,如果 length 的值大于 0,还要判断 length - 1 是否存在于 obj 上。举个最常见的例子:
var elems = document.getElementsByTagName('div');
这里的 elems 是什么?如果你如此使用过,那么应该知道 elems 有 length,可以如 elems[1] 这样来读取元素,实际上它并非是一个数组,它可能是一个 NodeList 或 HTMLCollection,但它能够通过上面的测试,所以是一个类数组对象。
// results 只供内部使用
makeArray: function( arr, results ) {
var ret = results || [];
if ( arr != null ) {
if ( isArraylike( Object(arr) ) ) {
jQuery.merge( ret,
typeof arr === "string" ?
[ arr ] : arr
);
} else {
core_push.call( ret, arr );
}
}
return ret;
}
makeArray 是将一个类数组变量转化成一个真正的数组。看到源码中的两个熟悉方法了吧:isArraylike 和 merge,上面都已经介绍过。不过在将 arr 传入 isArraylike 时使用了 Object(arr) 的方式。由于 isArraylike 中没有对参数是否为对象做检测,所以你可以传入基本类型,比如说字符串或数字,但这些内容在进行 (length - 1) in obj 时会引发错误,因此这里使用 Object(arr) 的方式将基本类型转成它们对应的封装类型(字符串是 String,数字是 Number)。
其实前面还忘了提一点,那就是字符串也有 length 属性。在 ES 3规范中,你无法使用 s[0] 这样的方法获取字符串 s 中的字符,只能使用 charAt,但在 ES 5中是允许这么做的。不管怎么说,一个字符串对象(String)可以通过 isArraylike 检测,但你不能将它当成一个真正的数组来使用,所以在执行 merge 前,判断了 arra 是否是字符串,如果是的话,把它放入一个数组中。
inArray: function( elem, arr, i ) {
var len;
if ( arr ) {
if ( core_indexOf ) {
return core_indexOf.call( arr, elem, i );
}
len = arr.length;
i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;
for ( ; i < len; i++ ) {
// 跳过访问稀疏数组
if ( i in arr && arr[ i ] === elem ) {
return i;
}
}
}
return -1;
}
该方法判断 elem 是否存在于数组 arr 中,你可以指定从数组的 i 下标开始查找。ES 5中定义了数组的 indexOf 方法,如果该方法存在,那么交给这个原生方法处理,否则就自己动手循环。
源码中的一个小技巧是针对稀疏数组的,什么是稀疏数组呢?
var arr = [];
arr[5] = '5';
这时候 arr 就是稀疏数组,因为此时 arr 中只有一个元素存在,但它的 length 值却是 6,如果你测试 '1' in arr 会返回 false,下标都不存在,那么对应的值就更不存在了,因此上面的方法中以同样的方式测试了下标是否存在于数组上。
如果没有找到对应的 elem,返回 -1。
grep: function( elems, callback, inv ) {
var retVal,
ret = [],
i = 0,
length = elems.length;
inv = !!inv;
// 遍历数组,存储通过验证函数的元素
for ( ; i < length; i++ ) {
retVal = !!callback( elems[ i ], i );
if ( inv !== retVal ) {
ret.push( elems[ i ] );
}
}
return ret;
}
grep 将函数中不符合要求的元素清除掉。它会返回一个新的数组,不会修改原数组。为了判断数组中哪些元素不符合要求,你需要传入一个过滤函数 callback,这个函数接收两个参数:数组元素和它在数组中的下标值,函数需要返回 true 或 false。
来看下 !! ,它其实就是两个 ! 操作符组合在了一起。!!inv 意味着 !(!inv)。操作符 ! 会将 inv 转成布尔值,并取反,再运行 !,表示再次取反,所以 !!inv 的意思就是将 inv 转为布尔值。这属于一种简便写法,很常用。
map: function( elems, callback, arg ) {
var value,
i = 0,
length = elems.length,
isArray = isArraylike( elems ),
ret = [];
if ( isArray ) {
for ( ; i < length; i++ ) {
value = callback( elems[ i ], i, arg );
if ( value != null ) {
ret[ ret.length ] = value;
}
}
} else {
for ( i in elems ) {
value = callback( elems[ i ], i, arg );
if ( value != null ) {
ret[ ret.length ] = value;
}
}
}
return core_concat.apply( [], ret );
}
这个方法看起来和 grep 很像,它将 callback 函数运行在数组的每个元素上,并将结果收集在一个新的数组中,最终将新数组返回。
由于源码和 grep 没什么大差异,不过多赘述了。值得注意的就是 ret[ret.length] = value。这是取代 ret.push(value) 的一种常见方式。
解析
这里的解析是指解析三种类型的文本:HTML、JSON 和 XML。先来看 HTML。
HTML
还记得在上一篇中,我们介绍 init 方法时,讲解的最后一个分支吗?
$('<span>')
当你如此传参时,就是在生成一个 span 元素。
if ( !data || typeof data !== "string" ) {
return null;
}
if ( typeof context === "boolean" ) {
keepScripts = context;
context = false;
}
context = context || document;
var parsed = rsingleTag.exec( data ),
scripts = !keepScripts && [];
// 单个标签
if ( parsed ) {
return [ context.createElement( parsed[1] ) ];
}
第一个判断就是判断 data 非空或是否为字符串。
context 参数指定了元素是由哪个上下文创建的,默认是 document(暂时没有想到这个参数还能传什么,后来查了查,虽然文档上没细说,但这里明确表示只能传 document)。该参数可以不传。
第三个参数和脚本有关,并且该方法后半部分的代码涉及了 buildFragments 这个看起来很复杂的方法,因为暂时还没看到,所以先不讲了。
关于上面的代码,我来说说 rsingTag 那个正则表达式吧,虽然它早在上一篇介绍 init 时就出现了,但我当时没讲。
rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/
这个正则用于匹配一个单独的标签:
<div>
<img />
<h3></h3>
正则中的 ^ 和 $ 应该比较熟悉了,略过。
< 因为是匹配标签,所以肯定是要以尖括号开头
(\w+) 这里面匹配的是标签名
\s* 匹配零个或多个空白,例如你可以传入 <div >
\/? \/ 是用来转义斜杠,因为有的标签是自闭和的,例如 <br /> 或 <img />
> 匹配第一个标签的结束
(?: 非捕获型括号,因为后面的内容捕获了也用不到
<\/\1> 这是匹配结束标签,\1 是引用了前面捕获的内容,即括号中的 \w+,看例子中的第三种情况
| 这表示分支条件,这个用于匹配例子中的前两种情况,即没有结束标签
)
JSON
JSON 应该是 web 开发中使用频率最高的一种数据类型了。注意,它只是一种格式,跟对象字面量没有什么关系,虽然它们可以相互转换。
ES 5 中增加了 JSON 对象,用于解析 JSON 格式。但是很可惜,在 jQuery 1.9.1 版本中,它还不能抛弃那些低版本的浏览器,否则的话,直接使用 window.JSON.parse 即可。这也是方法源码中第一段做的事情。
if ( window.JSON && window.JSON.parse ) {
return window.JSON.parse( data );
}
if ( data === null ) {
return data;
}
if ( typeof data === "string" ) {
// 确保字符串前后的空白字符被清除 (IE 无法处理它们)
data = jQuery.trim( data );
if ( data ) {
// 确保传入的 data 是真正的 JSON 格式
// 逻辑借鉴于 http://json.org/json2.js
if ( rvalidchars.test( data.replace( rvalidescape, "@" )
.replace( rvalidtokens, "]" )
.replace( rvalidbraces, "")) ) {
return ( new Function( "return " + data ) )();
}
}
}
jQuery.error( "Invalid JSON: " + data );
JSON 格式是由 Douglas Crockford 创建的,他还写了一个 JavaScript 版本的处理程序,在上面源码的注释中,链接已经失效,这里是最新的链接。具体的处理逻辑不谈,看看 new Function 那句。
我们知道定义函数的两种方式:函数声明,函数表达式。但其实还有第三种:new Function(arg1, arg2,... fnBody)。在这种方式中,你需要传入最少一个参数,作为函数体,如果传入多个参数,那么除了最后一个,其余均作为函数的参数。函数体是以字符串形式传进去的,这种方式是不是感觉很眼熟,想到 eval 了吗?
jQuery 为什么不选择 eval 呢?很早之前就有人问过这个问题,第一个答案解释的也很明确,两者的主要区别就是作用域,直接使用 eval,它的作用域与执行它的位置有关,而使用 new Function,则始终处于全局作用域下。除此之外,二者几乎没有差别。在下面介绍 globalEval 时会再讲讲 eval。
XML
parseXML: function( data ) {
var xml, tmp;
if ( !data || typeof data !== "string" ) {
return null;
}
try {
if ( window.DOMParser ) { // 标准
tmp = new DOMParser();
xml = tmp.parseFromString( data , "text/xml" );
} else { // IE
xml = new ActiveXObject( "Microsoft.XMLDOM" );
xml.async = "false";
xml.loadXML( data );
}
} catch( e ) {
xml = undefined;
}
if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) {
jQuery.error( "Invalid XML: " + data );
}
return xml;
}
这个没什么好讲解的,浏览器内置了解析 XML 的方法,只不过需要处理 IE 兼容问题。
额外方法
以下方法都是不知道怎么分类的。
noConflict: function( deep ) {
if ( window.$ === jQuery ) {
window.$ = _$;
}
if ( deep && window.jQuery === jQuery ) {
window.jQuery = _jQuery;
}
return jQuery;
}
conflict 是'冲突'的意思。$ 作为一个全局变量,如此简单明了,可能会被某些先引入的库或框架抢先使用,或者是页面引入了不同版本的 jQuery,那么大家都使用一个 $ 或 jQuery 全局变量,肯定要出问题。
在 core 文件最开始的变量声明处,你会看到如下内容:
_jQuery = window.jQuery,
_$ = window.$
这里使用两个变量预先将页面中的 jQuery 和 $ 对象保存起来。当你调用 noconflict 方法时,会将 window 对象上的这两个变量还原成它们的原始值,然后将 jQuery 自己的引用返回,你可以给这个引用赋予一个新的名字,以解决冲突。
now: function() {
return ( new Date() ).getTime();
}
返回当前时间,在 ES 5中,Date.now() 具有同样效果。
trim: core_trim && !core_trim.call("\uFEFF\xA0") ?
function( text ) {
return text == null ?
"" :
core_trim.call( text );
} :
function( text ) {
return text == null ?
"" :
( text + "" ).replace( rtrim, "" );
}
trim 方法用的比较频繁,它会将字符串前面和后面的空白字符清除掉。正因为使用频率较高,ES 5中增加了原生的 trim 方法。
error: function( msg ) {
throw new Error( msg );
}
抛出一个含有 msg 信息的异常
globalEval: function( data ) {
if ( data && jQuery.trim( data ) ) {
// Internet Explorer 中使用 execScript
// 我们使用一个匿名函数,这样在 Firefox 中,上下文为 window 而不是 jQuery 对象
( window.execScript || function( data ) {
window[ "eval" ].call( window, data );
} )( data );
}
}
eval 用于解析传入的参数,它就像一个内置的 JavaScript 解析器。使用 eval 的一个问题在于:它在哪个作用域下运行代码。
我们可以把 JavaScript 中的作用域分为全局作用域和局部作用域,而函数可以创建局部作用域,当你在全局作用域中使用 eval,那么它就在全局作用域内执行代码,否则就是在函数创建的局部作用域内。
如果我们想在全局作用域下运行 eval,可以使用 window.eval,但可惜的是,这里又碰到兼容性问题:IE 6,7,8。好在 IE 在出问题的同时又提供了解决方法:window.execScript。
注释中给了解决方法的原始链接,也可以参考 w3chelp 上的一篇文章。
// 将虚线形式转成驼峰形式,用于 css 和 data 模块
// Microsoft 忘记将前缀设置成驼峰形式 (#9572)
camelCase: function( string ) {
return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
}
所谓前缀就是诸如 -moz-、-webkit- 这样,它们最后应该转成 Moz 和 Webkit 形式,但是微软的 -ms- 需要转成 ms,所以这里特别处理下。详细信息可以查看编号为 9572 的 jQuery bug。(老看成 9527)
nodeName: function( elem, name ) {
return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
}
这个方法应该只是内部使用,在官网 API 上没看到。它的用途就是判断 elem 的 nodeName 与 name 是否一致。这里需要处理的问题是在 HTML 和 XML 中,元素的 nodeName 表现行为不一致。在 HTML 中,元素的 nodeName 始终返回大写形式,例如 'BODY'、'DIV',而在 XML 中,这和创建元素的标签值有关,详细信息可以查看 John Resig 的博客:.nodeName Case Sensitivity
proxy: function( fn, context ) {
var args, proxy, tmp;
if ( typeof context === "string" ) {
tmp = fn[ context ];
context = fn;
fn = tmp;
}
// 快速检测 target 是否能够调用。规范中规定应该抛出一个 TypeError 异常,但我们只是返回 undefined.
if ( !jQuery.isFunction( fn ) ) {
return undefined;
}
// 模拟 bind
args = core_slice.call( arguments, 2 );
proxy = function() {
return fn.apply( context || this, args.concat( core_slice.call( arguments ) ) );
};
// 将 fn 与 proxy 设置为相同的 guid,这样 proxy 可以被清除
proxy.guid = fn.guid = fn.guid || jQuery.guid++;
return proxy;
}
proxy 接收一个函数与上下文,返回一个始终运行在该上下文内的新函数。
其实很早之前,大家比较熟悉的名字应该是 bind。将函数绑定到特定的上下文内,而且 ES 5 中 Function 新增加的方法就叫做 bind,但是在 jQuery 中,bind 这个名字被事件处理的方法名给占用了。
proxy 的参数列表可以是函数与上下文,也可以是上下文和在上下文内的函数名。第一段判断就是为了区别这两种情况。
修改函数上下文最常用的的就是 call 与 apply,而模拟 bind 也是使用这两个方法,它们唯一的不同在于参数列表,如果你想在修改函数上下文的同时传递参数,那么在使用 call 方法时,参数需要一个一个的传入,而使用 apply,你只需要传入一个由参数组成的数组。所以当参数很少的时候,可以使用 call,如果参数很多,那么最好是组成一个数组,用 apply 方法传进去。
结语
这篇文章中的内容实在是太多了,但即便如此,我还是放弃了去讲 ready 和 access。主要原因自然是自己还没看完,但另一个原因在于,讲解 ready 需要其他内容,例如 promise,但它又是在另外的文件中定义的,而 access 方法还不到讲解的时候,虽然它在 jQuery 内部用途十分广泛。
接下来该看什么了呢?不知道,我也很期待下一篇:)