深浅拷贝

clone

一、数据类型

最新的 ECMAScript 标准定义了 8 种数据类型:

  • 7种原始类型(String、Number、Boolean、Null、Undefined、BigInt、Symbol):栈内存,使用 typeof 运算符检查。但 typeof null === "object"
  • 引用类型(Object):引用地址-栈内存,指向内容-堆内存,instanceof

二、浅拷贝

深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。浅拷贝,引用类型拷贝的就是内存地址。下面简单实现一个浅拷贝:

function shallowClone(obj) {
    const newObj = {};
    for(let prop in obj) {
        if(obj.hasOwnProperty(prop)){
            newObj[prop] = obj[prop];
        }
    }
    return newObj;
}

JavaScript中,浅拷贝的实现方式有:

  • Object.assign
  • Array.prototype.slice(), Array.prototype.concat()
  • 使用拓展运算符实现的复制

1. Object.assign()

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign()进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。

var obj = { a: {a: "kobe", b: 39} };
var initalObj = Object.assign({}, obj);
initalObj.a.a = "wade";
console.log(obj.a.a); //wade

注意:当object只有一层的时候,是深拷贝

let obj = {
    username: 'kobe'
    };
let obj2 = Object.assign({},obj);
obj2.username = 'wade';
console.log(obj);//{username: "kobe"}

2. slice()、concat()

补充说明:Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。

const fxArr = ["One", "Two", "Three", { name: 'Jimmy'}]
const fxArrs = fxArr.slice(0)
fxArrs[1] = "love";
fxArrs[3].name = 'Bob';
console.log(fxArr) // ["One", "Two", "Three", { name: 'Bob'}] // 基础类型未改变,引用类型的值改变了
console.log(fxArrs) // ["One", "love", "Three", { name: 'Bob'}]

3. 拓展运算符

const fxArr = ["One", "Two", "Three", { name: 'Jimmy'}]
const fxArrs = [...fxArr]
fxArrs[1] = "love";
fxArrs[3].name = 'Bob';
console.log(fxArr) // ["One", "Two", "Three", { name: 'Bob'}]
console.log(fxArrs) // ["One", "love", "Three", { name: 'Bob'}]

三、深拷贝

深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

目前实现深拷贝的方法不多,主要是两种:

1. JSON.parse(JSON.stringify())

原理: 用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。

undefinedfunctionsymbol、正则、 new Date()、数字对象 会在转换过程中被忽略或不好使。

const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj === originObj); // false

2. 手写递归

递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝

// 实现深度克隆---对象/数组
function deepClone(target) {
    // 初始化变量 result 成为最终克隆的数据;判断数据类型
    let result, targetType = checkedType(target);
    if (targetType === 'Object') {
        result = {};
    } else if (targetType === 'Array') {
        result = [];
    } else {
        return target;
    }
    // 遍历目标数据
    for (let i in target) {
        let value = target[i];
        if (checkedType(value) === 'Object' || checkedType(value) === 'Array') {
            result[i] = deepClone(value); // 递归遍历获取到value值
        } else {
            result[i] = value;
        }
    }
    return result;
}
// 定义检测数据类型的功能函数
function checkedType(target) {
    return Object.prototype.toString.call(target).slice(8, -1); // [object Object] | [object Array]
}

3. Lodash.cloneDeep()

该函数库也有提供_.cloneDeep用来做 Deep Copy

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f); // false

四、区别

赋值与浅拷贝

赋值:赋的其实是该对象在栈中的地址,而不是堆中的数据

浅拷贝:是按位拷贝对象,创建一个新对象,对原始对象属性值的一份精确拷贝。

// 赋值
const fxArr = ["One", "Two", "Three", { name: 'Jimmy'}];
const fxArrs = fxArr;
fxArrs[1] = "love";
fxArrs[3].name = 'Bob';
console.log(fxArr) // ["One", "love", "Three", { name: 'Bob'}] // 原对象一同改变
console.log(fxArrs) // ["One", "love", "Three", { name: 'Bob'}]

// 浅拷贝
const fxArr = ["One", "Two", "Three", { name: 'Jimmy'}];
const fxArrs = [...fxArr];
fxArrs[1] = "love";
fxArrs[3].name = 'Bob';
console.log(fxArr) // ["One", "Two", "Three", { name: 'Bob'}] // 基础类型未改变,引用类型的值改变了
console.log(fxArrs) // ["One", "love", "Three", { name: 'Bob'}]

深拷贝与浅拷贝

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

  • 浅拷贝:就只是复制对象的引用,如果拷贝后的对象发生变化,原对象也会发生变化。
  • 深拷贝:就是对目标的完全拷贝,引用和值都复制了。只要进行了深拷贝,它们老死不相往来,谁也不会影响谁。
类型与原数据是否指向同一对象值为基本数据类型时是否一同改变值为对象时是否一同改变
赋值
浅拷贝不变
深拷贝不变不变

参考文献

上次更新:
Contributors: jingmin.jiang, jiangjingmin, kyxiao