jQuery 源码阅读 —— core.js 续

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 &amp;&amp; length ) {
        return true;
    }

    return type === &quot;array&quot; || type !== &quot;function&quot; &amp;&amp;
        ( length === 0 ||
        typeof length === &quot;number&quot; &amp;&amp; length &gt; 0 &amp;&amp; ( 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 === &quot;string&quot; ?
                [ 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 &lt; 0 ? Math.max( 0, len + i ) : i : 0;

        for ( ; i &lt; len; i++ ) {
            // 跳过访问稀疏数组
            if ( i in arr &amp;&amp; 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 &lt; 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 &lt; 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 方法时,讲解的最后一个分支吗?

$('&lt;span&gt;')

当你如此传参时,就是在生成一个 span 元素。

if ( !data || typeof data !== &quot;string&quot; ) {
    return null;
}
if ( typeof context === &quot;boolean&quot; ) {
    keepScripts = context;
    context = false;
}
context = context || document;

var parsed = rsingleTag.exec( data ),
    scripts = !keepScripts &amp;&amp; [];

// 单个标签
if ( parsed ) {
    return [ context.createElement( parsed[1] ) ];
}

第一个判断就是判断 data 非空或是否为字符串。

context 参数指定了元素是由哪个上下文创建的,默认是 document(暂时没有想到这个参数还能传什么,后来查了查,虽然文档上没细说,但这里明确表示只能传 document)。该参数可以不传。

第三个参数和脚本有关,并且该方法后半部分的代码涉及了 buildFragments 这个看起来很复杂的方法,因为暂时还没看到,所以先不讲了。

关于上面的代码,我来说说 rsingTag 那个正则表达式吧,虽然它早在上一篇介绍 init 时就出现了,但我当时没讲。

rsingleTag = /^&lt;(\w+)\s*\/?&gt;(?:&lt;\/\1&gt;|)$/

这个正则用于匹配一个单独的标签:

&lt;div&gt;
&lt;img /&gt;
&lt;h3&gt;&lt;/h3&gt;

正则中的 ^ 和 $ 应该比较熟悉了,略过。

&lt;               因为是匹配标签,所以肯定是要以尖括号开头
    (\w+)       这里面匹配的是标签名
    \s*         匹配零个或多个空白,例如你可以传入 &lt;div   &gt;
    \/?         \/ 是用来转义斜杠,因为有的标签是自闭和的,例如 &lt;br /&gt; 或 &lt;img /&gt;
&gt;               匹配第一个标签的结束
(?:             非捕获型括号,因为后面的内容捕获了也用不到
    &lt;\/\1&gt;      这是匹配结束标签,\1 是引用了前面捕获的内容,即括号中的 \w+,看例子中的第三种情况
    |           这表示分支条件,这个用于匹配例子中的前两种情况,即没有结束标签
)

JSON

JSON 应该是 web 开发中使用频率最高的一种数据类型了。注意,它只是一种格式,跟对象字面量没有什么关系,虽然它们可以相互转换。

ES 5 中增加了 JSON 对象,用于解析 JSON 格式。但是很可惜,在 jQuery 1.9.1 版本中,它还不能抛弃那些低版本的浏览器,否则的话,直接使用 window.JSON.parse 即可。这也是方法源码中第一段做的事情。

if ( window.JSON &amp;&amp; window.JSON.parse ) {
    return window.JSON.parse( data );
}

if ( data === null ) {
    return data;
}

if ( typeof data === &quot;string&quot; ) {

    // 确保字符串前后的空白字符被清除 (IE 无法处理它们)
    data = jQuery.trim( data );

    if ( data ) {
        // 确保传入的 data 是真正的 JSON 格式
        // 逻辑借鉴于 http://json.org/json2.js
        if ( rvalidchars.test( data.replace( rvalidescape, &quot;@&quot; )
            .replace( rvalidtokens, &quot;]&quot; )
            .replace( rvalidbraces, &quot;&quot;)) ) {

            return ( new Function( &quot;return &quot; + data ) )();
        }
    }
}

jQuery.error( &quot;Invalid JSON: &quot; + 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 !== &quot;string&quot; ) {
        return null;
    }
    try {
        if ( window.DOMParser ) { // 标准
            tmp = new DOMParser();
            xml = tmp.parseFromString( data , &quot;text/xml&quot; );
        } else { // IE
            xml = new ActiveXObject( &quot;Microsoft.XMLDOM&quot; );
            xml.async = &quot;false&quot;;
            xml.loadXML( data );
        }
    } catch( e ) {
        xml = undefined;
    }
    if ( !xml || !xml.documentElement || xml.getElementsByTagName( &quot;parsererror&quot; ).length ) {
        jQuery.error( &quot;Invalid XML: &quot; + data );
    }
    return xml;
}

这个没什么好讲解的,浏览器内置了解析 XML 的方法,只不过需要处理 IE 兼容问题。

额外方法

以下方法都是不知道怎么分类的。

noConflict: function( deep ) {
    if ( window.$ === jQuery ) {
        window.$ = _$;
    }

    if ( deep &amp;&amp; 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 &amp;&amp; !core_trim.call(&quot;\uFEFF\xA0&quot;) ?
    function( text ) {
        return text == null ?
            &quot;&quot; :
            core_trim.call( text );
    } :
    function( text ) {
        return text == null ?
            &quot;&quot; :
            ( text + &quot;&quot; ).replace( rtrim, &quot;&quot; );
    }

trim 方法用的比较频繁,它会将字符串前面和后面的空白字符清除掉。正因为使用频率较高,ES 5中增加了原生的 trim 方法。

error: function( msg ) {
    throw new Error( msg );
}

抛出一个含有 msg 信息的异常

globalEval: function( data ) {
    if ( data &amp;&amp; jQuery.trim( data ) ) {
        // Internet Explorer 中使用 execScript
        // 我们使用一个匿名函数,这样在 Firefox 中,上下文为 window 而不是 jQuery 对象
        ( window.execScript || function( data ) {
            window[ &quot;eval&quot; ].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, &quot;ms-&quot; ).replace( rdashAlpha, fcamelCase );
}

所谓前缀就是诸如 -moz-、-webkit- 这样,它们最后应该转成 Moz 和 Webkit 形式,但是微软的 -ms- 需要转成 ms,所以这里特别处理下。详细信息可以查看编号为 9572 的 jQuery bug。(老看成 9527)

nodeName: function( elem, name ) {
    return elem.nodeName &amp;&amp; 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 === &quot;string&quot; ) {
        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 内部用途十分广泛。

接下来该看什么了呢?不知道,我也很期待下一篇:)