jQuery 源码学习(二)jQuery.fn和jQuery.extend()

先贴源码,这部分源码都在factory函数体内。

var deletedIds = [];

var slice = deletedIds.slice;

var concat = deletedIds.concat;

var push = deletedIds.push;

var indexOf = deletedIds.indexOf;

var class2type = {};

var toString = class2type.toString;

var hasOwn = class2type.hasOwnProperty;

var support = {};



var  
    version = "1.11.1",

    // Define a local copy of jQuery
    jQuery = function( selector, context ) {
        // The jQuery object is actually just the init constructor 'enhanced'
        // Need init if jQuery is called (just allow error to be thrown if not included)
        return new jQuery.fn.init( selector, context );
    },

    // Support: Android<4.1, IE<9
    // Make sure we trim BOM and NBSP
    rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,

    // Matches dashed string for camelizing
    rmsPrefix = /^-ms-/,
    rdashAlpha = /-([\da-z])/gi,

    // Used by jQuery.camelCase as callback to replace()
    fcamelCase = function( all, letter ) {
        return letter.toUpperCase();
    };

jQuery.fn = jQuery.prototype = {  
    // The current version of jQuery being used
    jquery: version,

    constructor: jQuery,

    // Start with an empty selector
    selector: "",

    // The default length of a jQuery object is 0
    length: 0,

    toArray: function() {
        return slice.call( this );
    },

    // Get the Nth element in the matched element set OR
    // Get the whole matched element set as a clean array
    get: function( num ) {
        return num != null ?

            // Return just the one element from the set
            ( num < 0 ? this[ num + this.length ] : this[ num ] ) :

            // Return all the elements in a clean array
            slice.call( this );
    },

    // Take an array of elements and push it onto the stack
    // (returning the new matched element set)
    pushStack: function( elems ) {

        // Build a new jQuery matched element set
        var ret = jQuery.merge( this.constructor(), elems );

        // Add the old object onto the stack (as a reference)
        ret.prevObject = this;
        ret.context = this.context;

        // Return the newly-formed element set
        return ret;
    },

    // Execute a callback for every element in the matched set.
    // (You can seed the arguments with an array of args, but this is
    // only used internally.)
    each: function( callback, args ) {
        return jQuery.each( this, callback, args );
    },

    map: function( callback ) {
        return this.pushStack( jQuery.map(this, function( elem, i ) {
            return callback.call( elem, i, elem );
        }));
    },

    slice: function() {
        return this.pushStack( slice.apply( this, arguments ) );
    },

    first: function() {
        return this.eq( 0 );
    },

    last: function() {
        return this.eq( -1 );
    },

    eq: function( i ) {
        var len = this.length,
            j = +i + ( i < 0 ? len : 0 );
        return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] );
    },

    end: function() {
        return this.prevObject || this.constructor(null);
    },

    // For internal use only.
    // Behaves like an Array's method, not like a jQuery method.
    push: push,
    sort: deletedIds.sort,
    splice: deletedIds.splice
};

jQuery.extend = jQuery.fn.extend = function() {  
    var src, copyIsArray, copy, name, options, clone,
        target = arguments[0] || {},
        i = 1,
        length = arguments.length,
        deep = false;

    // Handle a deep copy situation
    if ( typeof target === "boolean" ) {
        deep = target;

        // skip the boolean and the target
        target = arguments[ i ] || {};
        i++;
    }

    // Handle case when target is a string or something (possible in deep copy)
    if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
        target = {};
    }

    // extend jQuery itself if only one argument is passed
    if ( i === length ) {
        target = this;
        i--;
    }

    for ( ; i < length; i++ ) {
        // Only deal with non-null/undefined values
        if ( (options = arguments[ i ]) != null ) {
            // Extend the base object
            for ( name in options ) {
                src = target[ name ];
                copy = options[ name ];

                // Prevent never-ending loop
                if ( target === copy ) {
                    continue;
                }

                // Recurse if we're merging plain objects or arrays
                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 : {};
                    }

                    // Never move original objects, clone them
                    target[ name ] = jQuery.extend( deep, clone, copy );

                // Don't bring in undefined values
                } else if ( copy !== undefined ) {
                    target[ name ] = copy;
                }
            }
        }
    }

    // Return the modified object
    return target;
};

首先,jQuery 从 JavaScript 的原生数组和对象 API 里抽取了一些 API,如数组的 slice 、concat、push 和 index 方法,对象的 toString 和 hasOwnProperty 方法。
之后,开始声明 jQuery 的构造函数,或者叫 jQuery 类。可以看到 jQuery 的构造函数是很简单的,只是调用了他的 prototype 的 init 方法,去 new 一个实例。这个也很好理解,我们平时去生成一个 jQuery 对象的时候。从来没有 new JQuery(),而是 $() 或者 jQuery() ,其中的玄机就在于此。之后开始在原型上定义一些方法:

  • toArray 方法:调用数组的 slice 方法,将 jQuery 数组转化为真正的数组。
  • get 方法:如果传入了 num,则返回相应的 DOM 对象。如果不传入 num,则相当于调用了 toArray 方法,把自己变成了一个真正的数组。num 如果是负的,则从数组后面开始取值。
  • pushStack 方法:这个方法一个新的元素数组和一个新创建的 jQuery 对象通过 jQuery.merge 方法合并到一起,再把这个合并之后的 jQuery 对象返回,并且把之前的 jQuery 对象用新对象的 prevObject 保存起来。
  • each 方法,两个参数,分别是 callback 和 传入的参数,实际上调用的是 jQuery.each 方法,把实例对象传进去而已。
  • map 方法,调用了 jQuery.map 方法,有一点复杂,等看到 jQuery.map 方法时再说。
  • 将实例用原生的 slice 方法分割后,再用 pushStack 方法变回 jQuery 对象。
  • first 和 last:都是调用 eq 方法。
  • eq:一个参数 i,对 i 的取值做了一下处理(负值的话加数组的长度变正),如果处理后的 i 还在 0 到 length 之间,那么用 this[i] 取到对应的值,再用 pushStack 变成 jQuery 对象。
  • end:返回 prevObject,如果没有的话返回一个空的 jQuery 对象。
  • push, sort , splice:直接用的数组的原生方法,不返回 jQuery 对象,所以是当做内部方法使用的。

接下来,是 jQuery 中一个很重要的方法,extend 的定义,extend 的作用是可以很方便的合并多个对象,这在给对象赋值的时候十分方便。由于 jQuery 的方法是不传对象进去的,因此函数内部对于变量的类型和个数,做了很多种判断:

// Handle a deep copy situation
    if ( typeof target === "boolean" ) {
        deep = target;

        // skip the boolean and the target
        target = arguments[ i ] || {};
        i++;
    }

target 默认取的第一个参数,如果 target 是一个布尔值的话,那么就变成了是否是深拷贝的 flag ,赋值给真正的 flag deep。随后 target 转移到第二个参数上。

// Handle case when target is a string or something (possible in deep copy)
    if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
        target = {};
    }

当target既不是对象,而且也不是函数的时候,将 target 变成一个空对象。

// extend jQuery itself if only one argument is passed
    if ( i === length ) {
        target = this;
        i--;
    }

如果 i 和参数的个数相同,这说明只传入一个参数,或者两个参数,第一个是布尔值。 这个时候 target 就转换成了 jQuery 实例本身。

判断好了这些,接下来是一个大循环。跳出循环的条件是 i >= length,那么对于上面那种情况,这个循环是进入不了的,于是就直接把 target 给 return 了,所以直传一个对象进去,只会返回jQuery的实例,或者 jQuery 对象本身,并没什么用。要是什么都不传的话,按照上面的判断方式,length=0,i=1,所以返回的 target 应该是一个空对象。

当不是这些奇葩的情况时,那么循环开始,首先 options 取到 target 后一个参数,并且对 options 中的每一个属性开始遍历。这里开始区分是否是深拷贝的情况。如果是深拷贝的话使用到了递归,来逐级赋值。如果不是深拷贝的话,则是直接覆盖 target 对应的属性。

最后返回 target,值得注意的是 extend 方法是直接修改 target 对象的。