jQuery的extend方法源码解读

文章主要分为三部分,第一部分简单介绍了extend的语法,第二部分通过实例介绍extend的用途,最后一部分是extend的源码解读,同时另附extend的另一种实现方式。

一、方法介绍

jQuery 的 API 手册中,extend 方法挂载在 jQuery 和 jQuery.fn 两个不同的对象上,但在 jQuery 内部代码实现的是相同的,只是功能各不相同。

官方解释:

  • jQuery.extend:Merge the contents of two or more objects together into the first object.(把两个或者多个对象合并到第一个对象当中)

  • jQuery.fn.extend:Merge the contents of an object onto the jQuery prototype to provide new jQuery instance methods.(把对象挂载到 jQuery 的 prototype 上以扩展一个新的 jQuery 实例方法 。)

syntax:

  • jQuery.extend([deep,] [target,] object1 [,objectN]);

  • jQuery.fn.extend([deep,] [target,] object1 [,objectN])

deep: Boolen类型,可选,表示是否进行递归合并(深/浅复制),为true是为深复制;默认值为false,浅复制。
target:扩展对象,可选,将接收新的属性。
objectN:一个对象,包含额外的属性,扩展到目标对象(扩展对象)。

二、extend能实现的功能

将两个或者更多个对象合并到第一个对象

在这种情况下,extend方法需要至少传入两个对象,语法如下:

jQuery.extend(target, object1 [,objectN])
or
jQuery.fn.extend(target, object1 [,objectN])

合并object1,…,objectN对象内容到第一个对象target。
这里需要注意一下几点:

1.合并后target对象的内容会改变,如果不希望改变target对象的内容,可以将第一个对象设置为{}.
2.这种方法是有返回值的,返回值就是修改后的target对象.
3.合并后的target对象的内容,属性值永远是在object1,…,objectN几个对象中最后一次出现时的属性值,也就是对于相同名字的属性,后面对象中的属性值会覆盖前面对象的属性值。

实例

function getOpt(target, obj1, obj2, obj3){
    $.extend(target, obj1, obj2, obj3);
    return target;
}

var _default = {
    name : 'wenzi',
    age : '25',
    sex : 'male'
}
var obj1 = {
    name : 'obj1'
}
var obj2 = {
    name : 'obj2',
    age : '36'
}
var obj3 = {
    age : '67',
    sex : {'error':'sorry, I dont\'t kown'}
}
getOpt(_default, obj1, obj2, obj3);  // {name: "obj2", age: "67", sex: {error: "sorry, I dont't kown"}}

覆盖函数的默认参数

这条用法实际上就是用的“将两个或者更多个对象合并到第一个对象”,之所以把提出来另起一个标题,是因为这是一种很常见的编程技巧。

实例:

function getOpt(option){
    var _default = {
        name : 'wenzi',
        age : '25',
        sex : 'male'
    }
    $.extend(_default, option);
    return _default;
}
getOpt();  // {name: "wenzi", age: "25", sex: "male"}
getOpt({name:'bing'}); // {name: "bing", age: "25", sex: "male"}
getOpt({name:'bing', age:36, sex:'female'});  // {name: "bing", age: 36, sex: "female"}

函数getOpt含有默认参数(对象)_default,若传入函数的参数option(对象)中含有某个属性的值,则使用传入值,否则使用默认值。

深浅拷贝

所谓的深浅拷贝,就是C语言中的拷贝地址与数据

浅拷贝

浅复制对象A时,对象B将复制A的所有字段,如果字段是内存地址,B将复制地址,若果字段是基元类型,B将复制其值。
浅复制的缺点是如果你改变了对象B所指向的内存地址,你同时也改变了对象A指向这个地址的字段

function copy(target,cloneObj){
    for(var i in cloneObj){
        target[i]  = cloneObj[i];
    }
    return target;
}
var a = {
    a:{ c:"c" },
    b:"b"
}
var t = {};
copy(t,a);
t.a.c ="e";
console.log(a.a.c);//e

深拷贝

这种方式会完全复制所有数据,优点是B与A不会相互依赖(A,B完全脱离关联), 缺点是复制的速度慢,代价大。

一种是实现深度拷贝的方案:

function type(obj){
    return Object.prototype.toString.call(obj).slice(8,-1);
}
function deepCopy(target,cloneObj){
    var copy;
    for(var i in cloneObj){
        copy = cloneObj[i];
        if(target === copy){
            continue;
        }
        if(type(copy) === "Array"){
            target[i] = arguments.callee(target[i] || [],copy);
        }else if(type(copy) === "Object"){
            target[i] = arguments.callee(target[i] || {},copy);
        }else{
            target[i] = copy;
        }
    }
    return target;
}

var a = {
    a:{ c:"c" },
    b:"b"
}
var t = deepCopy({},a);
t.a.c ="e";
console.log(a.a.c);//c

注意关于arguments,caller,callee不懂的请移步这里JavaScript 之arguments、caller 和 callee 介绍 .
可以看到a没有被修改,但是要更深层次的遍历,肯定很耗费性能的。用for-in把所有可枚举的包括原型链上的一起遍历了。

用法

在用extend方法进行对象合并时,可以指定第一个参数为boolean类型,来决定是深拷贝(true)还是浅拷贝(false),语法如下:

jQuery.extend(deep, target, object1 [,objectN])
or
jQuery.fn.extend(deep, target, object1 [,objectN])

实例

var obj1 = {
    name: "John",
    location: {
        city: "Boston",
        county: "USA"
    }
}

var obj2 = {
    last: "Resig",
    location: {
        state: "MA",
        county: "China"
    }
}

$.extend(false, {}, obj1, obj2); // { name: "John", last: "Resig", location: { state: "MA", county: "China" }}

$.extend(true, {}, obj1, obj2); // { name: "John", last: "Resig", location: { city: "Boston", state: "MA", county: "China" }}

由此可见,执行 深度复制 会递归遍历每个对象中含有复杂对象(如:数组、函数、json对象等)的属性值进行复制,而且 浅度复制 便不会这么做。

jQuery插件开发

jQuery插件开发分为两种:1 类级别、2 对象级别

  • 类级别(类方法):是直接可以使用类引用,不需要实例化就可以使用的方法。一般在项目中 类方法 都是被设置为工具类使用;

  • 对象级别(实例方法)必须先创建实例,然后才能通过实例调用该 实例方法

jQuery可以看做是这个封装得非常好的类,而我们可以使用jQuery选择器来创建 jQuery 的实例。比如:使 id 选择器$(‘#btn’)来创建一个实例。

类级别 $.extend(src)

类级别你可以理解为拓展jQuery类,最明显的例子是$.ajax(...),相当于静态方法,开发扩展其方法时使用$.extend方法
实例1

$.extend({
    add:function(a,b){return a+b;} 
    minus:function(a,b){return a-b;}
}); 

调用方式

var i = $.add(3,2);
var j = $.minus(3,2); 

对象级别 $.fn.extend(src)

对象级别则可以理解为基于对象的拓展,如$("#table").set(...); 这里这个set呢,就是基于对象的拓展了。开发扩展其方法时使用$.fn.extend方法,

$.fn.extend({

    check:function(){
        return this.each({
            this.checked=true;
        });
    },
    uncheck:function(){
        return this.each({
            this.checked=false;
        });
    }
}); 

调用方式

$('input[type=checkbox]').check();
$('input[type=checkbox]').uncheck(); 

类似于命名空间的扩展

$.xy = {
    add:function(a,b){
        return a+b;
    } ,
    minus:function(a,b){
        return a-b;
    },
    voidMethod:function(){
        alert("void");
    }
};
调用方式
var i = $.xy.add(3,2);
var m = $.xy.minus(3,2);
$.xy.voidMethod(); 

用法

如果只有一个参数对象提供给$.extend(),这意味着目标参数被省略。在这种情况下,调用extend方法的对象被默认为目标对象,参数对象中的内容将合并到目标对象中去。语法如下:

jQuery.extend(object)
or
jQuery.fn.extend(object)
1.$.extend(src)

该方法就是将src合并到jquery的全局对象中去,如:

 $.extend({
  hello:function(){alert('hello');}
  });

就是将hello方法合并到jquery的全局对象中。

2.$.fn.extend(src)

该方法将src合并到jquery的实例对象中去,如:

 $.fn.extend({
  hello:function(){alert('hello');}
 });

就是将hello方法合并到jquery的实例对象中。

三、源码解读

// 为与源码的下标对应上,我们把第一个参数称为`第0个参数`,依次类推
jQuery.extend = jQuery.fn.extend = function() {
    var options, name, src, copy, copyIsArray, clone,
        target = arguments[0] || {}, // 默认第0个参数为目标参数
        i = 1,    // i表示从第几个参数凯斯想目标参数进行合并,默认从第1个参数开始向第0个参数进行合并
        length = arguments.length,
        deep = false;  // 默认为浅度拷贝

    // 判断第0个参数的类型,若第0个参数是boolean类型,则获取其为true还是false
    // 同时将第1个参数作为目标参数,i从当前目标参数的下一个
    // Handle a deep copy situation
    if ( typeof target === "boolean" ) {
        deep = target;

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

    //  判断目标参数的类型,若目标参数既不是object类型,也不是function类型,则为目标参数重新赋值 
    // Handle case when target is a string or something (possible in deep copy)
    if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
        target = {};
    }

    // 若目标参数后面没有参数了,如$.extend({_name:'wenzi'}), $.extend(true, {_name:'wenzi'})
    // 则目标参数即为jQuery本身,而target表示的参数不再为目标参数
    // Extend jQuery itself if only one argument is passed
    if ( i === length ) {
        target = this;
        i--;
    }

    // 从第i个参数开始
    for ( ; i < length; i++ ) {
        // 获取第i个参数,且该参数不为null,
        // 比如$.extend(target, {}, null);中的第2个参数null是不参与合并的
        // Only deal with non-null/undefined values
        if ( (options = arguments[ i ]) != null ) {

            // 使用for~in获取该参数中所有的字段
            // Extend the base object
            for ( name in options ) {
                src = target[ name ];   // 目标参数中name字段的值
                copy = options[ name ]; // 当前参数中name字段的值

                // 若参数中字段的值就是目标参数,停止赋值,进行下一个字段的赋值
                // 这是为了防止无限的循环嵌套,我们把这个称为,在下面进行比较详细的讲解
                // Prevent never-ending loop
                if ( target === copy ) {
                    continue;
                }

                // 若deep为true,且当前参数中name字段的值存在且为object类型或Array类型,则进行深度赋值
                // Recurse if we're merging plain objects or arrays
                if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
                    // 若当前参数中name字段的值为Array类型
                    // 判断目标参数中name字段的值是否存在,若存在则使用原来的,否则进行初始化
                    if ( copyIsArray ) {
                        copyIsArray = false;
                        clone = src && jQuery.isArray(src) ? src : [];

                    } else {
                        // 若原对象存在,则直接进行使用,而不是创建
                        clone = src && jQuery.isPlainObject(src) ? src : {};
                    }

                    // 递归处理,此处为2.2
                    // Never move original objects, clone them                      
                    target[ name ] = jQuery.extend( deep, clone, copy );

                // deep为false,则表示浅度拷贝,直接进行赋值
                // 若copy是简单的类型且存在值,则直接进行赋值
                // Don't bring in undefined values
                } else if ( copy !== undefined ) {
                    // 若原对象存在name属性,则直接覆盖掉;若不存在,则创建新的属性
                    target[ name ] = copy;
                }
            }
        }
    }

    // 返回修改后的目标参数
    // Return the modified object
    return target;
}; 

若参数中字段的值就是目标参数,停止赋值

在源码中进行了一下这样的判断:

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

为什么要有这样的判断,我们来看一个简单的例子,如果没有这个判断会怎么样:

var _default = {name : 'wenzi'};
var obj = {name : _default}
$.extend(_default, obj);
console.log(_default);

输出的_default是什么:

_default = {name : _default}; 

_default是object类型,里面有个字段name,值是_default,而_default是object类型,里面有个字段name,值是_default……,无限的循环下去。于是jQuery中直接不进行操作,跳过这个字段,进行下一个字段的操作。

深度拷贝时进行递归处理

变量值为简单类型(基元类型,如number, string, boolean)进行赋值时是不会影响上一个变量的值的,因此,如果当前字段的值为Object或Array类型,需要对其进行拆分,直到字段的值为简单类型(如number, string, boolean)时才进行赋值操作。

$.extend()与$.fn.extend()

jQuery.extend = jQuery.fn.extend = function(){}

也就是说$.extend()$.fn.extend()共用的是同一个函数体,所有的操作都是一样的,只不过两个extend使用的对象不同罢了:$.extend()是在jQuery($)上进行操作的;而$.fn.extend()是在jQuery对象上进行操作的,如$('div').extend().

四、另一种实现方式

版本1:

void function(global){
    var extend,
        _extend,
        _isObject;

    _isObject = function(o){
        return Object.prototype.toString.call(o) === '[object Object]';
    }

    _extend = function self(destination, source){
        for (var property in source) {
            if (source.hasOwnProperty(property)) {

                // 若sourc[property]是对象,则递归
                if (_isObject(source[property])) {

                    // 若destination没有property,赋值空对象
                    if (!destination.hasOwnProperty(property)) {
                        destination[property] = {};
                    };

                    // 对destination[property]不是对象,赋值空对象
                    if (!_isObject(destination[property])) {
                        destination[property] = {};
                    };

                    // 递归
                    self(destination[property], source[property]);
                } else {
                    destination[property] = source[property];
                };
            }
        }
    }

    extend = function(){
        var arr = arguments,
            result = {},
            i;

        if (!arr.length) return {};

        for (i = 0; i < arr.length; i++) {
            if (_isObject(arr[i])) {
                _extend(result, arr[i])
            };
        }

        arr[0] = result;
        return result;
    }

    global.extend = extend;
}(window)

版本1存在的问题:我们这里是按照参数顺序从左到右依次执行的,但是其实若是最后一个参数有的属性,前面的参数上的该属性都不需要再扩展了。其实前面的所有参数都是将自己身上有的属性而最后一个参数没有的属性补到最后一个参数上。既如此,是不是从参数列表的右侧开始扩展更好一些。

版本2

void function(global){
    var extend,
        _extend,
        _isObject;

    _isObject = function(o){
        return Object.prototype.toString.call(o) === '[object Object]';
    }

    _extend = function self(destination, source) {
        var property;
        for (property in destination) {
            if (destination.hasOwnProperty(property)) {

                // 若destination[property]和sourc[property]都是对象,则递归
                if (_isObject(destination[property]) && _isObject(source[property])) {
                    self(destination[property], source[property]);
                };

                // 若sourc[property]已存在,则跳过
                if (source.hasOwnProperty(property)) {
                    continue;
                } else {
                    source[property] = destination[property];
                }
            }
        }
    }

    extend = function(){
        var arr = arguments,
            result = {},
            i;

        if (!arr.length) return {};

        for (i = arr.length - 1; i >= 0; i--) {
            if (_isObject(arr[i])) {
                _extend(arr[i], result);
            };
        }

        arr[0] = result;
        return result;
    }

    global.extend = extend;
}(window)

五、参考

  1. 举例分析jQuery.extend()方法

  2. jquery中extend的实现

  3. 深入剖析 jQuery 的 extend 方法

  4. jQuery.extend 函数详解

  5. How to Create a Basic Plugin

  6. jQuery Plugin开发

  7. Jquery中extend该怎么写,有几种写法,分别用在什么场景?

  8. JavaScript 实现 extend

    原文作者:jack2wang
    原文地址: https://segmentfault.com/a/1190000005895616
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞