美文网首页
JS深拷贝与浅拷贝

JS深拷贝与浅拷贝

作者: 隔壁老王z | 来源:发表于2021-09-14 01:36 被阅读0次

    拷贝对象时,浅拷贝只解决了第一层的问题,拷贝第一层的 基本类型值,以及第一层的 引用类型地址。
    浅拷贝

    引用类型拷贝的地址
    浅拷贝的方法:
    1、Object.assign()
    
    2、展开语法 Spread
    
    3、Array.prototype.slice()
    

    深拷贝

    引用类型也重新拷贝
    JSON.parse(JSON.stringify(object))
    
    • JSON.parse(JSON.stringify(object))仍然有缺陷:
      1. 会忽略 undefined
      2. 会忽略 symbol
      3. 不能序列化函数
      4. 不能解决循环引用的对象
      5. 不能正确处理 new Date()
      6. 不能处理正则

    undefined、symbol 和函数这三种情况,会直接忽略

    let obj = {
        name: 'muyiy',
        a: undefined,
        b: Symbol('muyiy'),
        c: function() {}
    }
    console.log(obj);
    // {
    //  name: "muyiy", 
    //  a: undefined, 
    //  b: Symbol(muyiy), 
    //  c: ƒ ()
    // }
    
    let b = JSON.parse(JSON.stringify(obj));
    console.log(b);
    // {name: "muyiy"}
    

    循环引用情况下,会报错

    let obj = {
        a: 1,
        b: {
            c: 2,
            d: 3
        }
    }
    obj.a = obj.b;
    obj.b.c = obj.a;
    
    let b = JSON.parse(JSON.stringify(obj));
    // Uncaught TypeError: Converting circular structure to JSON
    

    new Date 情况下,转换结果不正确

    new Date();
    // Mon Dec 24 2018 10:59:14 GMT+0800 (China Standard Time)
    
    JSON.stringify(new Date());
    // ""2018-12-24T02:59:25.776Z""
    
    JSON.parse(JSON.stringify(new Date()));
    // "2018-12-24T02:59:41.523Z"
    

    解决方法转成字符串或者时间戳就好了:

    let date = (new Date()).valueOf();
    // 1545620645915
    
    JSON.stringify(date);
    // "1545620673267"
    
    JSON.parse(JSON.stringify(date));
    // 1545620658688
    

    正则会变成空对象

    let obj = {
        name: "muyiy",
        a: /'123'/
    }
    console.log(obj);
    // {name: "muyiy", a: /'123'/}
    
    let b = JSON.parse(JSON.stringify(obj));
    console.log(b);
    // {name: "muyiy", a: {}}
    

    如何模拟实现一个 Object.assign

    1. 判断 Object 是否支持该函数,如果不存在的话创建一个函数 assign ,并使用 Object.defineProperty 将该函数绑定到 Object 上;
    2. 判断参数是否正确(目标对象不能为空,我们可以直接设置 {} 传递进去,但必须设置值);
    3. 使用 Object() 转成对象,并保存为 to,最后返回这个对象 to;
    4. 使用 for...in 循环遍历出所有可枚举的自有属性,并复制给新的目标对象(使用 hasOwnProperty 获取自有属性,即非原型链上的属性)。
    if(typeof Object.assign !== 'function') {
      // Attention 1
      Object.defineProperty(Object, 'assign', {
        value: function(target) {
          'use strict'
          // undefined 和 null时  undefined == null (true)
          if(target == null) { // Attention 2
            throw new TypeError('Cannot convert undefined of null to object')
          }
          
          // Attention 3
          var to = Object(target)
      
          for(var index = 1; index < arguments.length; index++) {
            var nextSource = arguments[index]
            
            if(nextSource != null) { // Attention 2
              // Attention 4
              // 为什么用hasOwnProperty不用in?  in会检查原型链,hasOwnProperty不会:'toString' in {} // true
              // 但是直接使用 myObject.hasOwnProperty(..) 是有问题的,因为有的对象可能没有连接到 Object.prototype 上(比如通过 Object.create(null) 来创建),这种情况下,使用 myObject.hasOwnProperty(..) 就会失败。
              for(var nextKey in nextSource) {
                if(Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
                  to[nextKey] = nextSource[nextKey]
                }
              }
            }
          }
          
          return to
        },
        writable: true,
        configurable: true,
        enumerable: false
      })
    }
    

    实现一个深拷贝的一些问题
    考虑 对象、数组、循环引用、引用丢失、Symbol 和递归爆栈等情况
    简单实现: 递归 + 浅拷贝(浅拷贝时判断属性值是否是对象,如果是对象就进行递归操作)

    // 浅拷贝
    function cloneShallow(source) {
        var target = {};
        for (var key in source) {
            if (Object.prototype.hasOwnProperty.call(source, key)) {
                target[key] = source[key];
            }
        }
        return target;
    }
    // 深拷贝
    function cloneDeep1(source) {
        var target = {};
        for(var key in source) {
            if (Object.prototype.hasOwnProperty.call(source, key)) {
                if (typeof source[key] === 'object') {
                    target[key] = cloneDeep1(source[key]); // 注意这里
                } else {
                    target[key] = source[key];
                }
            }
        }
        return target;
    }
    

    这个实现方式的问题:

    1. 没有对传入参数进行校验,传入 null 时应该返回 null 而不是 {}
    2. 对于对象的判断不严谨,因为 typeof null === 'object'
    3. 没有考虑数组的兼容
    • 拷贝数组
      首先需要进行数据格式的校验,递归操作需要排除非引用类型的 source。
    function isObject(data) {
      return typeof data === 'object' && data !== null
    }
    function cloneDeep2(source) {
        // 非对象返回自身
        if (!isObject(source)) return source; 
       // 兼容数组
        var target = Array.isArray(source) ? [] : {};
        for(var key in source) {
            if (Object.prototype.hasOwnProperty.call(source, key)) {
                if (isObject(source[key])) {
                    target[key] = cloneDeep2(source[key]); // 注意这里
                } else {
                    target[key] = source[key];
                }
            }
        }
        return target;
    }
    
    • 循环引用
    // 1. 使用哈希表解决:
    // 其实就是循环检测,我们设置一个数组或者哈希表存储已拷贝的对象,当检测到当前对象在哈希表中存在时,即取出来并返回
    function cloneDeep3(source, hash = new WeakMap()) {
    
        if (!isObject(source)) return source; 
        if (hash.has(source)) return hash.get(source); // 新增代码,查哈希表
          
        var target = Array.isArray(source) ? [] : {};
        hash.set(source, target); // 新增代码,哈希表设值
        for(var key in source) {
            if (Object.prototype.hasOwnProperty.call(source, key)) {
                if (isObject(source[key])) {
                    target[key] = cloneDeep3(source[key], hash); // 新增代码,传入哈希表
                } else {
                    target[key] = source[key];
                }
            }
        }
        return target;
    }
    
    // 2. 使用数组,  在 ES5 中没有 weakMap 这种数据格式,所以在 ES5 中使用数组进行代替
    function cloneDeep3(source, uniqueList) {
        if (!isObject(source)) return source; 
        if (!uniqueList) uniqueList = []; // 新增代码,初始化数组
        var target = Array.isArray(source) ? [] : {};
        // ============= 新增代码
        // 数据已经存在,返回保存的数据
        var uniqueData = find(uniqueList, source);
        if (uniqueData) {
            return uniqueData.target;
        };
        // 数据不存在,保存源数据,以及对应的引用
        uniqueList.push({
            source: source,
            target: target
        });
        // =============
        for(var key in source) {
            if (Object.prototype.hasOwnProperty.call(source, key)) {
                if (isObject(source[key])) {
                    target[key] = cloneDeep3(source[key], uniqueList); // 新增代码,传入数组
                } else {
                    target[key] = source[key];
                }
            }
        }
        return target;
    }
    // 新增方法,用于查找
    function find(arr, item) {
        for(var i = 0; i < arr.length; i++) {
            if (arr[i].source === item) {
                return arr[i];
            }
        }
        return null;
    }
    
    • 拷贝 Symbol
      Symbol 在 ES6 下才有,我们需要一些方法来检测出 Symbol 类型。
      方法一:Object.getOwnPropertySymbols(...)
    // 查找一个给定对象的符号属性时返回一个 ?symbol 类型的数组。注意,每个初始化的对象都是没有自己的 symbol 属性的,因此这个数组可能为空,除非你已经在对象上设置了 symbol 属性。
    var obj = {};
    var a = Symbol("a"); // 创建新的symbol类型
    var b = Symbol.for("b"); // 从全局的symbol注册?表设置和取得symbol
    
    obj[a] = "localSymbol";
    obj[b] = "globalSymbol";
    
    var objectSymbols = Object.getOwnPropertySymbols(obj);
    
    console.log(objectSymbols.length); // 2
    console.log(objectSymbols)         // [Symbol(a), Symbol(b)]
    console.log(objectSymbols[0])      // Symbol(a)
    

    方法二:Reflect.ownKeys(...)

    // 返回一个由目标对象自身的属性键组成的数组。它的返回值等同于
    Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
    
    Reflect.ownKeys({z: 3, y: 2, x: 1}); // [ "z", "y", "x" ]
    Reflect.ownKeys([]); // ["length"]
    
    var sym = Symbol.for("comet");
    var sym2 = Symbol.for("meteor");
    var obj = {[sym]: 0, "str": 0, "773": 0, "0": 0,
               [sym2]: 0, "-1": 0, "8": 0, "second str": 0};
    Reflect.ownKeys(obj);
    // [ "0", "8", "773", "str", "-1", "second str", Symbol(comet), Symbol(meteor) ]
    // 注意顺序
    // Indexes in numeric order, 
    // strings in insertion order, 
    // symbols in insertion order
    

    方法一

    思路就是先查找有没有 Symbol 属性,如果查找到则先遍历处理 Symbol 情况,然后再处理正常情况,多出来的逻辑就是下面的新增代码。

    function cloneDeep4(source, hash = new WeakMap()) {
    
        if (!isObject(source)) return source; 
        if (hash.has(source)) return hash.get(source); 
    
        let target = Array.isArray(source) ? [] : {};
        hash.set(source, target);
    
        // ============= 新增代码
        let symKeys = Object.getOwnPropertySymbols(source); // 查找
        if (symKeys.length) { // 查找成功   
            symKeys.forEach(symKey => {
                if (isObject(source[symKey])) {
                    target[symKey] = cloneDeep4(source[symKey], hash); 
                } else {
                    target[symKey] = source[symKey];
                }    
            });
        }
        // =============
    
        for(let key in source) {
            if (Object.prototype.hasOwnProperty.call(source, key)) {
                if (isObject(source[key])) {
                    target[key] = cloneDeep4(source[key], hash); 
                } else {
                    target[key] = source[key];
                }
            }
        }
        return target;
    }
    

    方法二

    function cloneDeep4(source, hash = new WeakMap()) {
    
        if (!isObject(source)) return source; 
        if (hash.has(source)) return hash.get(source); 
    
        let target = Array.isArray(source) ? [] : {};
        hash.set(source, target);
    
        Reflect.ownKeys(source).forEach(key => { // 改动
            if (isObject(source[key])) {
                target[key] = cloneDeep4(source[key], hash); 
            } else {
                target[key] = source[key];
            }  
        });
        return target;
    }
    

    这里使用了 Reflect.ownKeys() 获取所有的键值,同时包括 Symbol,对 source 遍历赋值即可。

    写到这里已经差不多了,我们再延伸下,对于 target 换一种写法,改动如下。

    function cloneDeep4(source, hash = new WeakMap()) {
    
        if (!isObject(source)) return source; 
        if (hash.has(source)) return hash.get(source); 
    
        let target = Array.isArray(source) ? [...source] : { ...source }; // 改动 1
        hash.set(source, target);
    
        Reflect.ownKeys(target).forEach(key => { // 改动 2
            if (isObject(source[key])) {
                target[key] = cloneDeep4(source[key], hash); 
            }  // 改动3 else里面赋值也不需要了
        });
        return target;
    }
    
    • 递归爆栈问题
      上面使用的都是递归方法,但是有一个问题在于会爆栈,错误提示如下。
    // RangeError: Maximum call stack size exceeded
    

    那应该如何解决呢?其实我们使用循环就可以了,代码如下。

    function cloneDeep5(x) {
        const root = {};
    
        // 栈
        const loopList = [
            {
                parent: root,
                key: undefined,
                data: x,
            }
        ];
    
        while(loopList.length) {
            // 广度优先
            const node = loopList.pop();
            const parent = node.parent;
            const key = node.key;
            const data = node.data;
    
            // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
            let res = parent;
            if (typeof key !== 'undefined') {
                res = parent[key] = {};
            }
    
            for(let k in data) {
                if (data.hasOwnProperty(k)) {
                    if (typeof data[k] === 'object') {
                        // 下一次循环
                        loopList.push({
                            parent: res,
                            key: k,
                            data: data[k],
                        });
                    } else {
                        res[k] = data[k];
                    }
                }
            }
        }
    
        return root;
    }
    

    参考文章:木易杨前端进阶

    相关文章

      网友评论

          本文标题:JS深拷贝与浅拷贝

          本文链接:https://www.haomeiwen.com/subject/ezonsltx.html