深浅克隆(拷贝)

446 阅读3分钟

前言 js基础类型

  • 基本类型:Number、Boolean、String、null、undefined、symbol(ES6 新增的),BigInt(ES2020)
  • 引用类型:Object,对象子类型(Array,Function)

1. 浅克隆

克隆与被克隆对象指向同一段堆内存,下例obj与o互相影响

// 浅克隆函数
function shallowClone(o) {
  const obj = {};
  for ( let i in o) {
    obj[i] = o[i];
  }
  return obj;
}
// 被克隆对象
const oldObj = {
  a: 1,
  b: [ 'e', 'f', 'g' ],
  c: { h: { i: 2 } }
};

const newObj = shallowClone(oldObj);
console.log(newObj.c.h, oldObj.c.h); // { i: 2 } { i: 2 }
console.log(oldObj.c.h === newObj.c.h); // true

使用Object.assign(target, source)浅克隆

const target = { a: 1, b: 2 }
const source = { b: 4, c: 5 }

const returnedTarget = Object.assign(target, source)

target // { a: 1, b: 4, c: 5 }
returnedTarget // { a: 1, b: 4, c: 5 }

赋值(=)与浅拷贝的区别

    var obj1 = {
        'name' : 'zhangsan',
        'age' :  '18',
        'language' : [1,[2,3],[4,5]],
    };

    var obj2 = obj1;//赋值得到


    var obj3 = shallowCopy(obj1);//浅拷贝得到
    function shallowCopy(src) {
        var dst = {};
        for (var prop in src) {
            if (src.hasOwnProperty(prop)) {
                dst[prop] = src[prop];
            }
        }
        return dst;
    }

    obj2.name = "lisi";
    obj3.age = "20";

    obj2.language[1] = ["二","三"];
    obj3.language[2] = ["四","五"];

    console.log(obj1);  
    //obj1 = {
    //    'name' : 'lisi',
    //    'age' :  '18',
    //    'language' : [1,["二","三"],["四","五"]],
    //};

    console.log(obj2);
    //obj2 = {
    //    'name' : 'lisi',
    //    'age' :  '18',
    //    'language' : [1,["二","三"],["四","五"]],
    //};

    console.log(obj3);
    //obj3 = {
    //    'name' : 'zhangsan',
    //    'age' :  '20',
    //    'language' : [1,["二","三"],["四","五"]],
    //};

浅拷贝只复制一层对象的属性,并不包括对象里面的为引用类型的数据。

所以修改浅拷贝中的属性age时,不会修改原始数据,但language属性是一个数组,属于引用类型,浅拷贝不会复制出引用类型的数据。

基本数据类型:undefined,boolean,number,string,null

引用数据类型:object,包括有Array,function

深拷贝:将 B 对象拷贝到 A 对象中,包括 B 里面的子对象,

浅拷贝:将 B 对象拷贝到 A 对象中,但不包括 B 里面的子对象

2. 深克隆

JSON.parse方法

JSON对象parse方法可以将JSON字符串反序列化成JS对象,stringify方法可以将JS对象序列化成JSON字符串,这两个方法结合起来就能产生一个便捷的深克隆.

JSON.parse, JSON.stringfy详解

const newObj = JSON.parse(JSON.stringify(oldObj));
const oldObj = {
  a: 1,
  b: [ 'e', 'f', 'g' ],
  c: { h: { i: 2 } }
};
//下面就是深克隆 newObj与oldObj互不影响
const newObj = JSON.parse(JSON.stringify(oldObj));
console.log(newObj.c.h, oldObj.c.h); // { i: 2 } { i: 2 }
console.log(oldObj.c.h === newObj.c.h); // false
newObj.c.h.i = 'change';
console.log(newObj.c.h, oldObj.c.h); // { i: 'change' } { i: 2 }

但是它存在缺陷:

  1. 无法实现对函数、RegExp等特殊对象的克隆
  2. 会抛弃对象的constructor,所有的构造函数指向Object
  3. 对象有循环引用,会报错。

JSON.stringfy

1. JSON.stringify 方法会忽略对象的不可遍历属性

enumberable: false被设置的对应属性将会被忽略

2. JSON.stringify 方法的第二个参数

指定需要转成字符串的属性。

JSON.stringify({ a:1, b:2 }, ['a'])
// '{"a":1}'

更改默认的字符串化的行为。

function f(key, value) {
  if (typeof value === "number") {
    value = 2 * value;
  }
  return value;
}

JSON.stringify({ a:1, b:2 }, f)
// '{"a":2,"b":4}'
3. JSON.stringify 方法的第三个参数
  • 如果是数字,表示每个属性前面添加的空格(最多不超过10个);
  • 如果是字符串(不超过10个字符),则该字符串会添加在每行前面。

深克隆的实现

基础版本,不考虑数组和循环应用etc:

  • 如果是原始类型,无需继续拷贝,直接返回
  • 如果是引用类型,创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性执行深拷贝后依次添加到新对象上。
function clone(target) {
    if (typeof target === 'object') {
        let cloneTarget = {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

优化版:

const clone = parent => {
    const parent = []
    const children = []
    
    const _clone = parent => {
        if (parent === null) return null;
        if (typeof parent !== 'object') return parent;
        let child, proto;
        if(isType(parent, 'Array')) {
            //对数组做特殊处理
            child = []
        } else if (isType(parent, 'RegExp')) {
            //对正则对象特殊处理
            child = new RegExp(parent.source, getRegExp(parent));
            if (parent.lastIndex) child.lastIndex = parent.lastIndex;
        } else if (isType(parent, 'Date')) {
            // 对Date对象做特殊处理
            child = new Date(parent.getTime());
        } else {
            //处理对象类型
            proto = Object.getPrototypeOf(parent);
            //利用Object.create切断原型链
            child = Object.create(proto)
        }
        //处理循环引用
        const index = parents.indexOf(parent)
        if (index !== -1) {
            return children[index]
        }
        parents.push(parent)
        children.push(child)
        
        for (let i in parent) {
            child[i] = _clone(parent[i])
        }
        
        return child;
    }
    return _clone(parent)
}