# js深浅拷贝详解与实现

# JS中数据类型

  • 基本数据类型: Number、String、undefined、null、Boolean、和Symbol(ES6)
  • 引用数据类型: Object(Array, Function, Object, Date, RegExp, Math)

# 深浅拷贝

深浅拷贝只是针对引用类型的,因为引用类型是存放在堆内存中,在栈地址有一个或者多个地址来指向推内存的某一数据

对数据类型的内存存储还不明白的,可以看看我另外一篇文章 内存管理

# 什么是浅拷贝?

顾名思义,浅拷贝就是流于表面的拷贝方式;当属性值为对象类型时,只拷贝了对象数据的引用,导致新旧数据没有完全分离,还会互相影响

# 浅拷贝实现方式

# 1.= 赋值。

不多说,最基础的赋值方式,只是将对象的引用赋值。

let arr = [22, 44, 66, 88];
let newArr = arr;
newArr[0] = 11;
console.log(arr, newArr);

结果如图:

k

我们本来想把 arr 赋值给 newArr ,当我们修改newArr数组中第一个元素时,却发现了原始数组arr发生了改变 很显然,这就是一个浅拷贝的例子

原理:

  • 对于引用类型,赋值操作符只是把存放在栈内容中的指针赋值给另外一个变量。
  • 所以在赋值完成后,在栈内存就有两个指针指向堆内存同一个数据。
  • 也就可以说两个变量在共用着同一个数据,这就是浅拷贝。

# 2. 数组的concat() 或者 slice()

let arr1 = [1, [2, 2], 3];
let arr2 = arr1.concat();
arr2[0] = '哈';
arr2[1][1] = '变';
console.log('arr1 =>', arr1);
console.log('arr2 =>', arr2);
let arr1 = [1, [2, 2], 3];
let arr2 = arr1.slice(0);
arr2[0] = '哈';
arr2[1][1] = '变';
console.log('arr1 =>', arr1);
console.log('arr2 =>', arr2);

结果如图:

k2
  • 通过结果可以看出,concat()是可以复制一份新数组出来, 可当元素为对象类型时,只拷贝了对象数据的引用,导致新旧数据没有完全分离,
  • arr2[0]的元素是一个简单类型的值,改变它就不会影响原理的数组对应的值,
  • arr[1]的元素则是引用类型的值,所以改变arr[1][1]则也影响了原来数组的值

# 3.对象的Object.assign()

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

Object.assign(target, ...sources)

  • 参数:
  • target:目标对象。
  • sources:任意多个源对象。
  • 返回值:目标对象会被返回。
let obj = { a: {a: "hello", b: 21} };
let initalObj = Object.assign({}, obj);

initalObj.a.a = "changed";
console.log(obj.a.a); // "changed"

上面例子可以看到obj原对象的a.a的值也改变了

需要注意的是:

Object.assign()可以处理一层的深度拷贝,如下:

let obj1 = { a: 10, b: 20, c: 30 };
let obj2 = Object.assign({}, obj1);
obj2.b = 100;
console.log(obj1);
// { a: 10, b: 20, c: 30 } <-- 沒被改到
console.log(obj2);
// { a: 10, b: 100, c: 30 }

# 4.封装个浅拷贝函数(支持数组和对象)

function shallowCop(target){
    //判断参数是不是引用类型, 不是就原样返回
    if(typeof target !== 'object') return target;
    //判断目标类型,是数组就用[], 其他就用 {}
    var  newObj = target instanceof Array ? [] : {};

    for(var item in target){
        //只复制元素自身的属性,不复制原生链上的
        if(target.hasOwnProperty(item)){
            newObj[item] = target[item]
        }
    }
    return newObj;
}    

let test = [1, { name: 'yu' }];
let copyarr = shallowCop(test);
copyarr[1].name = 'YY';
console.log(test);
console.log(copyarr);

结果如图:

k3

# 5. 使用ES6扩展运算符 ...

let arr = [1 , [2, 3]]
let arr1 = [...arr];
arr[1][0] = 222;
console.log(arr); // [1, [222, 3]]
console.log(arr1);// [1, [222, 3]]

ES6扩展运算符,也只是深拷贝了一层

# 真正深拷贝的实现

从浅拷贝解释基本可以明白,深拷贝就是 ‘完全’拷贝,拷贝之后新旧数据完全分离,不再共用对象类型的属性值,不会互相影响。

# 1. JSON实现深拷贝

let obj1 = { body: { a: 10 } };
let obj2 = JSON.parse(JSON.stringify(obj1));
obj2.body.a = 20;
console.log(obj1);
// { body: { a: 10 } } <-- 沒被改到
console.log(obj2);
// { body: { a: 20 } }
console.log(obj1 === obj2);
// false
console.log(obj1.body === obj2.body);
// false

原理:

  • JSON.parse() 方法用于将一个 JSON 字符串转换为对象。
  • JSON.stringify() 方法用于将 JavaScript 值(通常为对象或数组)转换为 JSON 字符串。
  • 在JSON.stringify()完成后,对象就转为了字符串(简单类型),也就可以说实实在在的复制了一个值,不存在引用之说。
  • 再利用JSON.parse()转为对象,这样达到深拷贝的目的

这样做是真正的Deep Copy,这种方法简单易用。

但是这种方法也有不少坏处,譬如它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。

这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,即那些能够被 json 直接表示的数据结构。RegExp对象或者Function对象是无法通过这种方式深拷贝。

也就是说,只有可以转成JSON格式的对象才可以这样用,像function没办法转成JSON。

let obj = {
    nul: null,
    und: undefined,
    sym: Symbol('sym'),
    str: 'str',
    bol: true,
    num: 45,
    arr: [1, 4],
    reg: /[0-9]/,
    dat: new Date(),
    fun: function() {},  
}
console.log(JSON.parse(JSON.stringify(obj)))

结果如图:

k4

通过我们可以发现有些属性被忽略或者改变了:

  • dat对象改成字符串了,reg也改变了,
  • undefined, symbol,function 忽略了

可以看得出来JSON实现深拷贝也有不足之处

# 封装个递归深拷贝

function deepCopy(target) {
    // 判断不是引用类型则原样返回
    if (typeof target !== 'object') return target;
    //判断目标类型,是数组用 [], 其他用 {}
    var newObj = target instanceof Array ? [] : {};

    for (var item in target) {
        //只复制元素自身的属性,不复制原生链上的
        if (target.hasOwnProperty(item)) {
            //判断属性值类型,是引用类型则继续调用自身函数,其他直接赋值
            newObj[item] = typeof target[item] === 'object' ? deepCopy(target[item]) : target[item];
        }
    }
    return newObj;
}

let obj = {
    nul: null,
    und: undefined,
    sym: Symbol('sym'),
    str: 'str',
    bol: true,
    num: 45,
    arr: [1, 4],
    reg: /[0-9]/,
    dat: new Date(),
    fun: function () { },
}
let newObj = deepCopy(obj);
console.log(obj);
console.log(newObj);

结果如图:

k5

通过结果看出,拷贝虽然是拷贝了,可是Null类型,Date类型,RegExp类型的值都改变了,都改成了{},

所以我们得再优化下代码判断,最终代码如下:

function deepCopy(target) {
    // 判断不是引用类型则原样返回
    if (typeof target !== 'object') return target;
    //判断目标类型,是数组用 [], 其他用 {}
    var newObj = target instanceof Array ? [] : {};

    for (var item in target) {
        //只复制元素自身的属性,不复制原生链上的
        if (target.hasOwnProperty(item)) {
            //判断属性值类型,是引用类型(并且不是Null、Date、RegExp)则继续调用自身函数,其他直接赋值
            newObj[item] = 
                        typeof target[item] === 'object' && 
                        Object.prototype.toString.call(target[item]) !== '[object Date]' && 
                        Object.prototype.toString.call(target[item]) !== '[object Null]' && 
                        Object.prototype.toString.call(target[item]) !== '[object RegExp]' ? 
                        deepCopy(target[item]) : target[item];
            // newObj[item] = typeof target[item] === 'object' ? deepCopy(target[item]) : target[item];
        }
    }
    return newObj;
}

let obj = {
    nul: null,
    und: undefined,
    sym: Symbol('sym'),
    str: 'str',
    bol: true,
    num: 45,
    arr: [1, 4],
    reg: /[0-9]/,
    dat: new Date(),
    fun: function () { },
}
let newObj = deepCopy(obj);
console.log(obj);
console.log(newObj);

结果如图:

k6

哦耶,完美深拷贝,所有数据类型都覆盖到

参考文献

js浅拷贝与深拷贝方法 (opens new window)

深入理解JS深浅拷贝 (opens new window)

感谢大家耐心看完,以上纯属个人见解,各位小伙伴有什么想法,更好的办法,欢迎留言交流

更新时间: 2021年2月6日星期六晚上7点30分