浅拷贝(shallow copy)与深拷贝(deep copy)

297 阅读3分钟

浅拷贝(shallow copy)与深拷贝(deep copy)

深浅拷贝都是对于引用数据类型而言的,对于基本数据类型的拷贝,并没有深浅拷贝的区别。

浅拷贝

  • 仅复制最外一层,对于内层是相同的引用。
  • 对于基本数据类型的属性,拷贝值。源对象和拷贝对象开辟不同的内存空间,不共享。
  • 对于引用数据类型的属性,拷贝内存地址。源对象和拷贝对象指向同一块内存空间,共享。

深拷贝

  • 在堆内存中开辟新的空间存储数据对象,源对象和拷贝对象存储地址不同,不共享。

赋值 与 浅拷贝

赋值

  • 基本数据类型:在栈内存空间创建一块数据对象,被变量引用,不同变量引用的内存不同。
  • 引用数据类型:赋值时变量引用的是存储在栈内存中的一个地址,这个地址指向存储在堆空间的引用数据类型对象。即对变量的赋值是赋给地址,堆空间的数据共享。

区别

针对引用数据类型的赋值与浅拷贝存在区别:

  1. 赋值时,这个引用数据类型变量的内存数据是共享的,它的基本数据类型属性改变了,源对象和拷贝对象都会改变。

  2. 浅拷贝时,对于基本数据类型的属性,拷贝值,不共享不会一起变化。引用数据类型属性,拷贝地址,内存数据共享,会联动变化。

  3. 举例:

    const source = {
        A: 'A',
        referProp: ['A','B','C']
    }
    ​
    // 赋值
    const obj1 = source;
    obj1.A = 'newA';
    obj1.referProp[0] = 'newA'
    console.log(source.A)        // newA
    console.log(source.referProp) // ['newA','B','C'],都改变了// 浅拷贝
    const obj2 = {...source}
    obj2.A = 'newA';
    obj2.referProp[0] = 'newA'
    console.log(source.A)        // A  --不改变
    console.log(source.referProp) // ['newA','B','C']  --改变
    

浅拷贝实现

在 JavaScript 中,所有标准的内置对象复制操作(展开语法Array.prototype.concat()Array.prototype.slice()Array.from()Object.assign()Object.create())创建的都是浅拷贝。

扩展运算符

const source = {
    A: 'A',
    referProp: ['A','B','C']
}
// 浅拷贝
const result = {...source}

Object.assign

const source1 = {
    A: 'A',
    referProp: ['A','B','C']
}
const source2 = { B: 'B' }
// 浅拷贝
const result = Object.assign({}, source1, source2)
// {A: 'A', referProp: ['A','B','C'], B: 'B'}

原生数组方法

Array.prototype.concat()

const arrSource1 =  ['A' ,['a','b','c'], 'C']
const arrSource2 =  ['a' ,['1','2','3'], 'c']
const result = arrSource1.concat(arrSource2);

Array.prototype.slice()

const arrSource =  ['A' ,['a','b','c'], 'C']
​
const result = arrSource1.slice();

Array.from()

const arrSource =  ['A' ,['a','b','c'], 'C']
​
const result = Array.from(arrSource);

手写

function shallowCopy (target) {
    // 基本类型直接返回
    if (!target || typeof target !== "object") return target;
    // 判断对象or数组
    let result = Array.isArray(params) ? [] : {};
    // 遍历拷贝属性
    for (let key in target) {
        if (target.hasOwnProperty(key)) {  // 只拷贝target自有的属性
            result[key] = target[key];
        }
    }
    return result;
}

深拷贝实现

函数库lodash

const _ = require('lodash');
const source = {
   A: 'A',
    referProp: ['A','B','C']
};
const result = _.cloneDeep(source);

JSON.stringify

使用 JSON.stringify() 将可以被序列化的对象转换为 JSON 字符串,然后使用 JSON.parse() 将该字符串转换回(全新的)JavaScript 对象。

const source = {
   A: 'A',
    referProp: ['A','B','C']
};
const result = JSON.parse(JSON.stringify(source));

存在问题

  • 拷贝的对象中如果有 function、undefined、symbol,当使用过JSON.stringify()进行处理之后,都会消失。
  • 无法拷贝不可枚举的属性;
  • 无法拷贝对象的原型链;
  • 拷贝 Date 引用类型会变成字符串;
  • 拷贝 RegExp 引用类型会变成空对象;
  • 对象中含有NaN、Infinity以及 -InfinityJSON 序列化的结果会变成null
  • 无法拷贝对象的循环引用,即对象成环 (obj[key] = obj)。

手写

只拷贝基本数据类型、数组Array、对象{}

参考:前端面试 第三篇 js之路 深拷贝与浅拷贝 - 掘金 (juejin.cn)

// 检测数据类型的功能函数
const checkedType = (target) => Object.prototype.toString.call(target).replace(/[object (\w+)]/, "$1").toLowerCase();
// 实现深拷贝(仅仅为Object/Array)
const clone = (target, hash = new WeakMap) => {
    let result;
    let type = checkedType(target);
    if (type === 'object') result = {};
    else if (type === 'array') result = [];
    else return target;
    let copyObj = new target.constructor();
    if (hash.has(target)) return hash.get(target);
    hash.set(target, copyObj)
    for (let key in target) {
        if (checkedType(target[key]) === 'object' || checkedType(target[key]) === 'array') {
            result[key] = clone(target[key], hash);
        } else {
            result[key] = target[key];
        }
    }
    return result;
}

兼容处理多种类型,基本满足使用

// 分别要处理
// 1. 基本类型;
// 2. 引用类型:数组、对象、map、set(可循环)
// 3. 不可循环类型:基本类型的包装类型(Boolean、Number、String、Symbol) 和 部分引用类型(Function、RegExp、Date)
// 可循环类型
const traverseTypes = ['array', 'object', 'map', 'set', 'argument'];
// 检测数据类型的功能函数
const checkedType = (target) => Object.prototype.toString.call(target).replace(/[object (\w+)]/, "$1").toLowerCase();
// 拷贝RegExp的方法
const cloneRegExp = (source) => {
    const reFlags = /\w*$/;
    const result = new source.constructor(source.source, reFlags.exec(source));
    result.lastIndex = source.lastIndex;
    return result;
}
// 拷贝不可遍历的对象类型
const cloneOtherType = (obj, type) => {
    switch (type) {
        case 'boolean':
        case 'number':
        case 'string':
        case 'date':
            return new obj.constructor(obj.valueOf());
        case 'symbol':
            return Object(obj.valueOf());
        case 'regexp':
            return cloneRegExp(obj);
        case 'function':          // function不做处理
            return obj;
    }
}
// 实现深拷贝
const deepClone = (target, map = new WeakMap) => {
    const type = checkedType(target);
    // 1. 处理基本数据类型,直接返回
    if (target instanceof Object === false) return target
    let result;
    // 2.
    if (traverseTypes.includes(type)) {
        // 如果是可遍历类型,直接创建空对象
        result = new obj.constructor();
    } else {
        // 若不是,则走特殊处理
        return cloneOtherType(target, type);
    }
    // 3. 解决循环引用问题
    result = new target.constructor();
    if (map.has(target)) return map.get(target);
    map.set(target, result)
    /* ----------------处理Map、Set的深拷贝---------------- */
    // 4. 处理Map类型
    if (type === 'map') {
        target.forEach((value, key) => {
            result.set(key, deepClone(value, map))
        })
        return result
    }
    // 5. 处理Set类型
    if (type === 'set') {
        target.forEach(value => {
            result.add(deepClone(value, map))
        })
        return result
    }
    /* ----------------处理数组和对象{}类型---------------- */
    // 6. 处理数组和对象{}
    for (let key in target) {
        if (target.hasOwnProperty(key)) {
            result[key] = deepClone(target[key], map);
        }
    }
    return result;
}
const obj = {
    // 基本类型
    str: 'test',
    num1: 123,
    boolean: true,
    sym: Symbol('独一无二key'),
    // 引用类型(以下8种数据对象均需进行真正意义上的深拷贝)
    obj_object: { name: 'squirrel' },
    arr: [123, [1, 23, 2], '456'],
    func: (name, age) => console.log(`姓名:${name},年龄:${age}岁`),
    map: new Map([['t', 100], ['s', 200]]),
    set: new Set([1, 2, 3]),
    date: new Date(),
    reg: new RegExp(/test/g),
    // 包装类
    num2: new Number(123),
}
obj.loop = obj;
const objCopy = deepClone(obj);
obj.set.add(123)
obj.arr[0] = 999
obj.loop.arr[0] = 888
obj.func = []   // 改变源对象,不会影响深拷贝的copy对象
console.log(obj, 'target')
console.log(objCopy, 'result')
console.log(obj.loop == obj)        // true
console.log(objCopy.loop == objCopy) // true

image.png

参考