资讯专栏INFORMATION COLUMN

JS的深浅拷贝

xiaoxiaozi / 2411人阅读

摘要:引用类型之所以会出现深浅拷贝的问题,实质上是由于对基本类型和引用类型的处理不同。另外方法可以视为数组对象的浅拷贝。上面描述过的复杂问题依然存在,可以说是最简陋但是日常工作够用的深拷贝方式。

一直想梳理下工作中经常会用到的深拷贝的内容,然而遍览了许多的文章,却发现对深拷贝并没有一个通用的完美实现方式。因为对深拷贝的定义不同,实现时的edge case过多,在深拷贝的时候会出现循环引用等问题,导致JS内部并没有实现深拷贝,但是我们可以来探究一下深拷贝到底有多复杂,各种实现方式的优缺点,同时参考下常用库对其的实现。

引用类型

之所以会出现深浅拷贝的问题,实质上是由于JS对基本类型和引用类型的处理不同。基本类型指的是简单的数据段,而引用类型指的是一个对象,而JS不允许我们直接操作内存中的地址,也就是不能操作对象的内存空间,所以,我们对对象的操作都只是在操作它的引用而已。

在复制时也是一样,如果我们复制一个基本类型的值时,会创建一个新值,并把它保存在新的变量的位置上。而如果我们复制一个引用类型时,同样会把变量中的值复制一份放到新的变量空间里,但此时复制的东西并不是对象本身,而是指向该对象的指针。所以我们复制引用类型后,两个变量其实指向同一个对象,改变其中一个对象,会影响到另外一个。

var num = 10;
var obj = {
    name: "Nicholas"
}

var num2 = num;
var obj2 = obj;

obj.name = "Lee";
obj2.name; // "Lee"

可以看到我们的obj和obj2都保存了一个指向该对象的指针,所有的操作都是对该引用的操作,所以对对象的修改会影响其他的复制对象。

浅拷贝

如果我们要复制对象的所有属性都不是引用类型时,就可以使用浅拷贝,实现方式就是遍历并复制,最后返回新的对象。

function shallowCopy(obj) {
    var copy = {};
    // 只复制可遍历的属性
    for (key in obj) {
        // 只复制本身拥有的属性
        if (obj.hasOwnProperty(key)) {
            copy[key] = obj[key];
        }
    }
    return copy;
}

如上面所说,我们使用浅拷贝会复制所有引用对象的指针,而不是具体的值,所以使用时一定要明确自己的需求,同时,浅拷贝的实现也是最简单的。

JS内部实现了浅拷贝,如Object.assign(),其中第一个参数是我们最终复制的目标对象,后面的所有参数是我们的即将复制的源对象,支持对象或数组,一般调用的方式为

var newObj = Object.assign({}, originObj);

这样我们就得到了一个新的浅拷贝对象。另外[].slice()方法可以视为数组对象的浅拷贝。

深拷贝

如果我们需要复制一个拥有所有属性和方法的新对象,就要用到深拷贝,JS并没有内置深拷贝方法,主要是因为:

深拷贝怎么定义?我们怎么处理原型?怎么区分可拷贝的对象?原生DOM/BOM对象怎么拷贝?函数是新建还是引用?这些edge case太多导致我们无法统一概念,造出大家都满意的深拷贝方法来。

内部循环引用怎么处理,是不是保存每个遍历过的对象列表,每次进行对比,然后再造一个循环引用来?这样带来的性能消耗可以接受吗。

解释一些常见的问题概念,防止有些同学不明白我们在讲什么。比如循环引用:

var obj = {};
obj.b = obj;

这样当我们深拷贝obj对象时,就会循环的遍历b属性,直到栈溢出。
我们的解决方案为建立一个集合[],每次遍历对象进行比较,如果[]中已存在,则证明出现了循环引用或者相同引用,我们直接返回该对象已复制的引用即可:

let hasObj = [];
function referCopy(obj) {
    let copy = {};
    hasObj.push(obj);
    for (let i in obj) {
        if (typeof obj[i] === "object") {
            let index = hasObj.indexOf(obj[i]);
            if (index > -1) {
                console.log("存在循环引用或属性引用了相同对象");
                // 如果已存在,证明引用了相同对象,那么无论是循环引用还是重复引用,我们返回引用就可以了
                copy[i] = hasObj[index];
            } else {
                copy[i] = referCopy(obj[i]);
            }
        } else {
            copy[i] = obj[i];
        }
    }
    return copy;
}

处理原型和区分可拷贝的对象:我们一般使用function.prototype指代原型,使用obj.__proto__指代原型链,使用enumerable属性表示是否可以被for ... in等遍历,使用hasOwnProperty来查询是否是本身元素。在原型链和可遍历属性和自身属性之间存在交集,但都不相等,我们应该如何判断哪些属性应该被复制呢?

函数的处理:函数拥有一些内在属性,但我们一般不修改这些属性,所以函数一般直接引用其地址即可。但是拥有一些存取器属性的函数我们怎么处理?是复制值还是复制存取描述符?

var obj = {
    age: 10,
    get age() {
        return this.age;
    },
    set age(age) {
        this.age = age;
    }
};
var obj2 = $.extend(true, {}, obj);

obj2; // {age: 10}

这个是我们想要的结果吗?大部分场景下不是吧,比如我要复制一个已有的Vue对象。当然我们也有解决方案:

function copy(obj) {
    var copy = {};
    for (var i in obj) {
        let desc = Object.getOwnPropertyDescriptor(obj, i);
        // 检测是否为存取描述符
        if (desc.set || desc.get) {
            Object.defineProperty(copy, i, {
                get: desc.get,
                set: desc.set,
                configuarable: desc.configuarable,
                enumerable: true
            });
        // 否则为数据描述符,则复用下面的深拷贝方法,此处简写
        } else {
            copy[i] = obj[i];
        }
    }
    return copy;
}

虽然边界条件很多,但是不同的框架和库都对该方法进行了实现,只不过定义不同,实现方式也不同,如jQuery.extend()只复制可枚举的属性,不继承原型链,函数复制引用,内部循环引用不处理。而lodash实现的就更为优秀,它实现了结构化克隆算法
该算法的优点是:

可以复制 RegExp 对象。

可以复制 Blob、File 以及 FileList 对象。

可以复制 ImageData 对象。CanvasPixelArray 的克隆粒度将会跟原始对象相同,并且复制出来相同的像素数据。

可以正确的复制有循环引用的对象

依然存在的缺陷是:

Error 以及 Function 对象是不能被结构化克隆算法复制的;如果你尝试这样子去做,这会导致抛出 DATA_CLONE_ERR 的异常。

企图去克隆 DOM 节点同样会抛出 DATA_CLONE_ERROR 异常。

对象的某些特定参数也不会被保留

RegExp 对象的 lastIndex 字段不会被保留

属性描述符,setters 以及 getters(以及其他类似元数据的功能)同样不会被复制。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write,因为这是默认的情况下。

原形链上的属性也不会被追踪以及复制。

我们先来看看常规的深拷贝,它跟浅拷贝的区别在于,当我们发现对象的属性是引用类型时,进行递归遍历复制,直到遍历完所有属性:

var deepClone = function(currobj){
    if(typeof currobj !== "object"){
        return currobj;
    }
    if(currobj instanceof Array){
        var newobj = [];
    }else{
        var newobj = {}
    }
    for(var key in currobj){
        if(typeof currobj[key] !== "object"){
            // 不是引用类型,则复制值
            newobj[key] = currobj[key];
        }else{
            // 引用类型,则递归遍历复制对象
            newobj[key] = deepClone(currobj[key])    
        }
    }
    return newobj
}

这个的主要问题就是不处理循环引用,不处理对象原型,函数依然是引用类型。上面描述过的复杂问题依然存在,可以说是最简陋但是日常工作够用的深拷贝方式。

另外还有一种方式是使用JSON序列化,巧妙但是限制更多:

// 调用JSON内置方法先序列化为字符串再解析还原成对象
newObj = JSON.parse(JSON.stringify(obj));

JSON是一种表示结构化数据的格式,只支持简单值、对象和数组三种类型,不支持变量、函数或对象实例。所以我们工作中可以使用它解决常见问题,但也要注意其短板:函数会丢失,原型链会丢失,以及上面说到的所有缺陷。

库实现

上面的两种方式可以满足大部分场景的需求,如果有更复杂的需求,可以自己实现。现在我们可以看一些框架和库的解决方案,下面拿经典的jQuery和lodash的源码看下,它们的优缺点上面都说过了:

jQuery.extend()
// 进行深度复制,如果第一个参数为true则深度复制,如果目标对象不合法,则抛弃并重构为{}空对象,如果只有一个参数则功能为扩展jQuery对象
jQuery.extend = jQuery.fn.extend = function() {
    var options, name, src, copy, copyIsArray, clone,
        target = arguments[ 0 ] || {},
        i = 1,
        length = arguments.length,
        deep = false;

    // Handle a deep copy situation
    // 第一个参数可以为true来确定进行深度复制
    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
    // 如果只有一个参数,扩展jQuery对象
    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
                // 阻止最简单形式的循环引用
                // var obj={}, obj2={a:obj}; $.extend(true, obj, obj2); 就会形成复制的对象循环引用obj
                if ( target === copy ) {
                    continue;
                }
                // 如果为深度复制,则新建[]和{}空数组或空对象,递归本函数进行复制
                // Recurse if we"re merging plain objects or arrays
                if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
                    ( copyIsArray = Array.isArray( copy ) ) ) ) {

                    if ( copyIsArray ) {
                        copyIsArray = false;
                        clone = src && Array.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;
};
lodash _.baseClone()
/**
     * The base implementation of `_.clone` and `_.cloneDeep` which tracks
     * traversed objects.
     *
     * @private
     * @param {*} value The value to clone.
     * @param {boolean} bitmask The bitmask flags.
     *  1 - Deep clone
     *  2 - Flatten inherited properties
     *  4 - Clone symbols
     * @param {Function} [customizer] The function to customize cloning.
     * @param {string} [key] The key of `value`.
     * @param {Object} [object] The parent object of `value`.
     * @param {Object} [stack] Tracks traversed objects and their clone counterparts.
     * @returns {*} Returns the cloned value.
     */
    function baseClone(value, bitmask, customizer, key, object, stack) {
      var result,
          isDeep = bitmask & CLONE_DEEP_FLAG,
          isFlat = bitmask & CLONE_FLAT_FLAG,
          isFull = bitmask & CLONE_SYMBOLS_FLAG;

      if (customizer) {
        result = object ? customizer(value, key, object, stack) : customizer(value);
      }
      if (result !== undefined) {
        return result;
      }
      if (!isObject(value)) {
        return value;
      }
      var isArr = isArray(value);
      if (isArr) {
        result = initCloneArray(value);
        if (!isDeep) {
          return copyArray(value, result);
        }
      } else {
        var tag = getTag(value),
            isFunc = tag == funcTag || tag == genTag;

        if (isBuffer(value)) {
          return cloneBuffer(value, isDeep);
        }
        if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
          result = (isFlat || isFunc) ? {} : initCloneObject(value);
          if (!isDeep) {
            return isFlat
              ? copySymbolsIn(value, baseAssignIn(result, value))
              : copySymbols(value, baseAssign(result, value));
          }
        } else {
          if (!cloneableTags[tag]) {
            return object ? value : {};
          }
          result = initCloneByTag(value, tag, baseClone, isDeep);
        }
      }
      // Check for circular references and return its corresponding clone.
      stack || (stack = new Stack);
      var stacked = stack.get(value);
      if (stacked) {
        return stacked;
      }
      stack.set(value, result);

      var keysFunc = isFull
        ? (isFlat ? getAllKeysIn : getAllKeys)
        : (isFlat ? keysIn : keys);

      var props = isArr ? undefined : keysFunc(value);
      arrayEach(props || value, function(subValue, key) {
        if (props) {
          key = subValue;
          subValue = value[key];
        }
        // Recursively populate clone (susceptible to call stack limits).
        assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack));
      });
      return result;
    }
参考资料

知乎 JS的深拷贝和浅拷贝: https://www.zhihu.com/questio...

Javascript之深拷贝: https://aepkill.github.io/201...

js对象克隆之谜:http://b-sirius.me/2017/08/26...

知乎 JS如何完整实现深度Clone对象:https://www.zhihu.com/questio...

github lodash源码:https://github.com/lodash/lod...

MDN 结构化克隆算法:https://developer.mozilla.org...

jQuery v3.2.1 源码

JavaScript高级程序设计 第4章(变量、作用域和内存问题)、第20章(JSON)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/88766.html

相关文章

  • JS专题之深浅拷贝

    摘要:在之前的文章专题之数据类型和类型检测中我有讲过,中的数据类型分为两种,基本数据类型和引用数据类型,基本数据类型是保存在栈的数据结构中的是按值访问,所以不存在深浅拷贝问题。 前言 在开发过程中,偶尔会遇到这种场景,拿到一个数据后,你打算对它进行处理,但是你又希望拷贝一份副本出来,方便数据对比和以后恢复数据。 那么这就涉及到了 JS 中对数据的深浅拷贝问题,所谓深浅拷贝,浅拷贝的意思就是,...

    ASCH 评论0 收藏0
  • 深入理解JS深浅拷贝

    摘要:深拷贝相比于浅拷贝速度较慢并且花销较大。所以在赋值完成后,在栈内存就有两个指针指向堆内存同一个数据。结果如下扩展运算符只能对一层进行深拷贝如果拷贝的层数超过了一层的话,那么就会进行浅拷贝那么我们可以看到和展开原算符对于深浅拷贝的结果是一样。 JS中数据类型 基本数据类型: undefined、null、Boolean、Number、String和Symbol(ES6) 引用数据类型:...

    JackJiang 评论0 收藏0
  • 9012年,当我们讨论js深浅拷贝时我们在说些什么?

    摘要:正文讨论深浅拷贝,首先要从的基本数据类型说起根据中的变量类型传递方式,分为值类型和引用类型,值类型变量包括。当你拷贝的对象有多级的时候,就是深拷贝。数据不存在则对其拷贝。 前言: 本文主要阅读对象:对深浅拷贝印象模糊对初级前端,想对js深浅拷贝聊一聊的中级前端。 如果是对这些有完整对认知体系和解决方法的大佬,可以选择略过。 正文: 讨论深浅拷贝,首先要从js的基本数据类型说起: 根据 J...

    xeblog 评论0 收藏0
  • 深浅拷贝

    摘要:深复制实现代码如下第一种方法通过递归解析解决第二种方法通过解析解决作者六师兄链接原生深拷贝的实现处理未输入新对象的情况通过方法构造新的对象 深浅拷贝针对的是 对象类型,如果是字符串的数组用[...arr],还是不会影响 要区分针对数组的深浅拷贝(默认情况为里面没有对象的数组),与针对对象的深浅拷贝 JavaScript数组深拷贝和浅拷贝的两种方法 let a1 = [1, 2]; ...

    Karrdy 评论0 收藏0
  • 复习Javascript专题(四):js深浅拷贝

    摘要:基本数据类型的复制很简单,就是赋值操作,所以深浅拷贝也是针对,这类引用类型数据。它会抛弃对象的。另外,查资料过程中还看到这么一个词结构化克隆算法还有这一篇资料也有参考,也写得比较详细了的深浅拷贝 基本数据类型的复制很简单,就是赋值操作,所以深浅拷贝也是针对Object,Array这类引用类型数据。 浅拷贝对于字符串来说,是值的复制,而对于对象来说则是对对象地址的复制;而深拷贝的话,它不...

    MobService 评论0 收藏0
  • 一篇文章彻底搞懂JS深浅拷贝和const

    摘要:图数据类型图引用类型深浅拷贝问题不知道什么是深拷贝和浅拷贝的请先去并在调试台自己操作一下,这篇文章只会说明为何中会有这种问题。所以有的时候我们为了避免浅拷贝,会用一些方式实现深拷贝。 首先要了解的js基础 基本数据类型:Object、undefined、null、Boolean、Number、String、Symbol (ES6新加) Object包括: Array 、Date 、R...

    MyFaith 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<