深浅拷贝

138 阅读4分钟

对象类型在赋值过程中其实是复制的引用地址,所以在一方改变时,另一方去访问值也是修改后的。

const a = {
    text: '这是a'
}
const b = a;
b.text = '这是b'

浅拷贝

Object.assign

大多人认为这个函数是用来深拷贝的,但是当对象的属性是对象类型时,复制的依然是地址,所以不是深拷贝

const a = {
    name: '我是a',
    info: {
        age: 18
    }
}
const b = Object.assign({}, a);
b.name = '我是b';
b.info.age = 24;

console.log(a.name); // ‘我是a’
console.log(a.info.age); // 24

es6的解构

const a = {
    name: '我是a'
}
const b = {...a};

深拷贝

JSON.parse(JSON.stringify(obj));

const a = {
    name: '我是a',
    info: {
        height: 190,
        weight: 140
    }
}
const b = JSON.parse(JSON.stringify(a));
b.name = '我是b';
b.info.height = 160;

console.log(a.name, b.name); // 输出:'我是a' '我是b'
console.log(a.info.height, b.info.height); // 输出:190 140

缺点:

  • 忽略undefined
  • 忽略Symbol
  • 不能序列化函数
const a = {
    name: '我是a',
    info: undefined,
    id: Symbol('a'),
    job: function(){}
}
const b = JSON.parse(JSON.stringify(a));

console.log(b); // 输出: {name: '我是a'}
  • 不能解决循环引用的问题
const obj = {
    a: 'a',
    b: {
        c: 1,
        d: 2
    }
}
obj.c = obj.b;
obj.e = obj.a;
obj.b.c = obj.c;
obj.b.e = obj.e;
const newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj);

image.png

MessageChannel

如果拷贝的对象含有内置类型且不包含函数,则可以使用MessageChannel

function structualClone (obj) {
    return new Promise(resolve => {
        const {port1, port2} = new MessageChannel();
        port2.onmessage = en => resolve(en.data);
        port1.postMessage(obj);
    });
}
async function clone (obj) {
    const res = await structualClone(obj);
    console.log(res);
    return res;
}
const obj = {
    a: 'a',
    b: {
        c: 'c'
    }
};
obj.b.d = obj.b;
clone(obj);

自定义深拷贝函数

第一步:简单实现

深拷贝可以拆分成2步,浅拷贝 + 递归,浅拷贝是判断属性值是否对象,如果是对象就进行递归操作,两个一结合就实现了深拷贝。

function cloneShallow(source) {
   var target = {};
   for(let key in source) {
       if(source.hasOwnProperty(key)) {
           target[key] = source[key];
       }
   }
   return target;
}

cloneShallow实现了一个浅拷贝,只要稍微改动下,加上是否是对象的判断并在相应位置使用堤溃就可以实现简单的深拷贝。

function cloneDeep1(source) {
    let target = {};
    for(let key in source) {
        if(source.hasOwnProperty(key)) {
            if(typeof source[key] === 'object') {
                target[key] = cloneDeep1(source[key]);
            }
            else {
                target[key] = source[key];
            }
        }
    }
    return target;
}

一个简单的深拷贝就完成了,但是这个实现存在较多的问题。

  1. 没有对传入的数据,进行数据校验
  2. 对于对象的判断逻辑不够严谨,因为typeof null === 'object'
  3. 没有考虑数组的兼容

第二步:拷贝数组

我们来对对象的判断

function isObject(obj) {
    return typeof obj === 'object' && obj !== null;
}

所以兼容数组的写法如下。

function cloneDeep2(source) {
    if(!isObject(source)) {
        return source;
    }
    var target = Array.isArray(source) ? [] : {};
    for(let key in source) {
        if(source.hasOwnProperty(key)) {
            target[key] = cloneDeep2(source[key]);
        }
    }
    return target;
}

// 测试
var a = {
   name: "bajiu",
   book: {
       title: "JS",
       price: "45"
   },
   a1: undefined,
   a2: null,
   a3: 123
}
var b = cloneDeep2(a);

第三步:解决循环应用问题

JSON无法深拷贝循环引用,遇到这种情况就会抛出异常。

a.circleRef = a;
JSON.parse(JSON.stringify(a));
// Uncaught TypeError: Converting circular structure to JSON

我们设置一个数组或者哈希表存储已拷贝过的对象,当检测到当前对象已存在哈希表中是,去除该值并返回即可。

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(let key in source) {
        if(source.hasOwnProperty(key)) {
            target[key] = cloneDeep3(source[key], hash)
        }
    }
    return target;
}

第四步:拷贝Symbol

我们需要一些方法来检测出Symbol类型

  • 方法一:Object.getOwnPropertySymbol(……),查找给定队形的符号属性时返回symbol类型的数组。
let obj = {};
let a = Symbol('a');
let b = Symbol.for('b');
obj[a] = 'localSymbol';
obj[b] = 'globalSymbol';
let symbols = Object.getOwnPropertySymbols(obj);
console.log(symbols); // [Symbol(a), Symbol(b)]
  • 方法二:Reflect.ownKeys(……),返回一个有目标对象自身属性键组成的数组,它的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbold(target))
Reflect.ownKeys({x: 1, z: 2, y: 3}); // ['x', 'y', 'z']
Reflect.ownKeys([]); // ['length']
let sym = Symbol.for('comet');
let sym2 = Symbol.for('meteor');
let obj = {
    8: 'test8'
    0: 'test1',
    [sym]: 0,
    str: 'str1',
    second: 'str2',
    [sym2]: 1
}
Reflect.ownKeys(obj);
//  ['0', '8', 'str', 'second', Symbol(comet), Symbol(meteor)]
// 输出顺序
// indexes in numberic order
// strings in insertion order
// symbols in insertion order
function cloneDeep4(source, hash = new WeakMap()) {
    if(!isObjec(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 => {
        target[key] = cloneDeep4(source[key], hash);
    })
    return target;
}

第五步:解决递归爆栈

上面几步中都用到了递归的方法,存在一个问题--爆栈,我们可以利用循环解决这个问题,代码如下

function deepClone5(source) {
    const root = {};
    const loopList = [{
        parent: root,
        key: undefined,
        data: source
    }]
    while(loopList.length) {
        const node = loopList.pop();
        const parent = node.parent;
        const key = node.key;
        const data = node.data;
        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;
}