JS深拷贝与浅拷贝

2021-09-14  本文已影响0人  隔壁老王z

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

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

深拷贝

引用类型也重新拷贝
JSON.parse(JSON.stringify(object))

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. 没有考虑数组的兼容
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 属性的,因此这个数组可能为空,除非你已经在对象上设置了 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;
}

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

上一篇下一篇

猜你喜欢

热点阅读