深拷贝的实现

为什么会有深拷贝浅拷贝?

这牵扯到JavaScript中的数据类型。一种是基本类型,包括5中基本类型UndefinedNullBooleanStringNumber。这些类型的变量是按值存放的,可以直接访问。

另一种是引用类型,例如对象、数组等,这些变量实际上保存着值得一个引用,通过这个引用来找到值。

所以,浅拷贝就是直接赋值给新的变量,对于引用类型,也仅仅拷贝其一个引用而已。这个实现起来比较容易。

function copy(obj) {
    var newObj = {};
    for (var key in obj) {
        newObj[key] = obj[key];
    }
    return newObj;
}

ES6中Object.assign方法其实也是一个浅拷贝,它会将源对象可枚举的属性逐个赋值给新的对象。

浅拷贝会出现什么问题呢?因为引用类型的变量始终保存的都是一个引用,所以浅拷贝中的新的引用对象与源引用对象指向的还是一个对象,那么如果修改其任意一个对象的属性,在另一个指向该对象的变量中,也会被修改掉。

var oldObj = {prop: {innerProp: 'test prop'}};
var newObj = copy(oldObj);
console.log(newObj);  //{ prop: { innerProp: 'test prop' } }
oldObj.prop.innerProp = 'changed';
console.log(oldObj);  //{ prop: { innerProp: 'changed' } }
console.log(newObj);  //{ prop: { innerProp: 'changed' } }

解决变量引用的问题,可以使用深拷贝。

解决问题的思路是将所有的引用类型,通过不断的递归调用,将引用类型转换为基本数据值类型保存即可。

(function ($) {
    "use strict";

    var types = 'Array Object String Date RegExp Function Boolean Number Null Undefined'.split(' ');

    function type() {
        return Object.prototype.toString.call(this).slice(8, -1);
    }

    for (var i = types.length; i--;) {
        $['is' + types[i]] = (function (self) {
            return function (elem) {
                return type.call(elem) === self;
            }
        })(types[i]);
    }

    function deepCopy(deep, obj) {
        if (obj === null || (typeof obj !== 'object' && !$.isFunction(obj))) {
            return obj;
        }

        if ($.isFunction(obj)) {
            return new Function("return " + obj.toString())();
        } else {
            var name, target = $.isArray(obj) ? [] : {}, value;

            for (name in obj) {
                value = obj[name];

                //var a = {name: b}
                //var b = {name: a}
                //var c = $.extend(a, b)
                //相互引用时,防止无限展开
                if (value === obj) {
                    continue;
                }

                if (deep && ($.isArray(value) || $.isObject(value))) {
                    if (!$.isFunction(value)) {
                        target[name] = deepCopy(deep, value);
                    } else {
                        target[name] = new Function("return " + value.toString())();
                    }
                } else if (value !== undefined) {
                    //过滤掉undefined的属性值
                    target[name] = value;
                }
            }
            return target;
        }
    }

    var testObj = [1, 2, {
        word: 'hello',
        name: 'Jason',
        c: undefined
    }, [2, 3, 4, 5]];

    var objCopied = deepCopy(true, testObj);
    console.log('objCopied', objCopied);//objCopied [ 1, 2, { word: 'hello', name: 'Jason' }, [ 2, 3, 4, 5 ] ]
    testObj[2].name = '张三';
    console.log('objOrigin', testObj);//objOrigin [ 1,2,{ word: 'hello', name: '张三', c: undefined },[ 2, 3, 4, 5 ] ]
    console.log('objCopied', objCopied);//objCopied [ 1, 2, { word: 'hello', name: 'Jason' }, [ 2, 3, 4, 5 ] ]
}({}));

上述方法所示,当类型为基本数据类型时,直接返回保存数据,当为Function类型时,新建一个Function对象返回,当为引用类型时,再次调用自身,将引用类型传入,直到返回基本数据类型为止。

参考jQuery源码,将上述代码修改为可传递多参数的方式。

MyQuery.extend = MyQuery.fn.extend = function () {
    var i = 1,
        options, //非target参数对象,用于缓存当前遍历对象
        src, //待拷贝数据对象,来自options
        copy, //target中的数据对象
        clone, //引用类型对象副本
        name,
        length = arguments.length || 0,
        target = arguments[0] || {},
        deep = false;

    if (typeof target === 'boolean') {
        deep = target;
        //第一个参数为Boolean,使用第二个参数作为target
        target = arguments[1] || {};
        //为之后的循环跳过为boolean的参数位置
        i++;
    }

    //参数比较奇怪的情况 $.extend('a string', {obj: 'an object'})
    if (typeof target !== 'object' && Object.prototype.toString.call(target) !== '[object Function]') {
        target = {};
    }

    //$.extend(obj)情况
    if (length === i) {
        target = this;
        i--;
    }

    for (; i < length; i++) {
        if ((options = arguments[i]) !== null) {
            for (name in options) {
                src = options[name];
                copy = target[name];

                //防止循环引用无限展开
                //var obj1 = {a: obj2}
                //var obj2 = {b: obj1}
                //$.extend(obj1, obj2)
                if (copy === options) {
                    continue;
                }

                if (deep) {
                    if (Object.prototype.toString.call(src) === '[object Object]' || Object.prototype.toString.call(src) === '[object Array]') {
                        clone = Object.prototype.toString.call(src) === '[object Object]' ? src : {};
                        clone = Object.prototype.toString.call(src) === '[object Array]' ? src : [];
                        target[name] = this.extend(deep, clone, src);
                    } else if (Object.prototype.toString.call(src) === '[object Function]') {
                        target[name] = new Function('return ' + src.toString())();
                    } else {
                        target[name] = src;
                    }
                } else if (src !== undefined) {
                    target[name] = src
                }
            }
        }
    }
    return target;
};

文章参考地址:https://github.com/wengjq/Blog/issues/3

如果您觉得本文对您有用,欢迎捐赠或留言~
微信支付
支付宝

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注