蒋小昆的博客
做一个安静的小开发

也谈 JS 中的深浅拷贝(一)

|

缘起

在JS中,值类型和引用类型的处理是特别容易出问题的地方

知识储备

  • JS中基础数据类型有7种,其中原始数据类型:String,Number,Boolean,Null,Undefined,Symbol(ES6新增),和 Object

    声明: new String() 和 typeof Null === ‘object’ 等特殊表现不在讨论范围之内

  • 按照在内存中的位置可以分为值类型引用类型,其中值类型存放在栈(Stack)内存中,而引用类型存放在堆(Heap)内存中

JS 中能够实现 copy 功能的

  1. Array 中的 slice, concat, …运算符,Array.from, 还有 .map 等
  2. Object 中的 JSON.parse(JSON.stringify()), Object.assign 等

当数组中的项均为 原始数据类型 的时候,都能复制出一个表现一样的数组 当对象中的 value 均为 原始数据类型 的时候,都能复制出一个表现一样的对象 本文中着重讨论当数组或者对象中的项 非原始数据类型 的时候,各种 copy 的 差别

基础例子

```
var o1 = {
    name: 'test1',
    desc: '这是一个最基础的对象'
};
var o2 = {
    name: 'test2',
    desc: '这是一个最基础的对象'
};
var o3 = {
    name: 'test3',
    desc: '这是一个最基础的对象'
};
var objArr = [
    o1, o2, o3
];
```

slice/concat/…/Array.from/.map

    var t_slice = objArr.slice();
    var t_concat = objArr.concat();
    var t_spread = [...objArr];
    var t_arrayfrom = Array.from(objArr);
    var t_map = objArr.map((item) => {
        return item;
    });
    var t_assign = Object.assign({}, objArr);
    var t_stringify = JSON.parse(JSON.stringify(objArr));
    objArr[0].name = 'new name';
    objArr[0] = null;
    console.log(t_slice[0].name);// output: new name
    console.log(t_concat[0].name);// output: new name
    console.log(t_spread[0].name);// output: new name
    console.log(t_arrayfrom[0].name);// output: new name
    console.log(t_map[0].name);// output: new name
    console.log(t_assign[0].name);// output: new name
    console.log(t_stringify[0].name);// output: test1

本例中只以数组为例,因为数组也是 Object,通过数组的表现很容易推到出 单纯的 Object 的表现

当数组中的项为对象的时候,objArr = [0x1111, 0x1112, 0x1113], 而 slice/concat 等相当于 将 地址 进行了copy

值得注意的是执行了 objArr[0] = null; 对最终的结果没有影响,因为置 null只是把指针打断了,对内存中的对象实体没有影响

总结

  • slice/concat/…/Array.from/.map/Object.assign等复制方法,本质上都是浅拷贝,即只拷贝了引用地址(而不是实际的对象)
  • JSON.parse(JSON.stringify()),能实现 JSON格式 数据的深拷贝,但也有其弊端:
    • 无法处理循环引用
    • function 或者 RegExp 等类型无法进行深拷贝(而且会直接丢失相应的值)
    • 在处理 null/undefined/NaN 等特殊类型时会有异常表现
        var old = {
        k1: null,
        k2: undefined,
        k3: {
            arr: []
        },
        k4: {
            arr: [null, undefined, NaN]
        },
        k5: NaN
        };
      

      经过处理之后: Object.keys结果发生了改变,原来的 undefined/NaN 等被转成了 null ```javascript var new = JSON.parse(JSON.stringify(old)); /* { k1: null, k3: { arr: [] } k4: { arr: [null, null, null] } } */

    ```

    • 对象的 constructor 也会丢失,无论原来的构造函数(类)是什么,均变成 Object,执行 instanceof 原构造函数 会返回 false
  1. 如果是用 Angular,可以使用 angular.copy
  2. 如果是用 jQuery,可以使用 $.extend(true, {}, source);
  3. 如果是用 lodash,可以使用 _.cloneDeep()

分享一个目前在用相对健壮的深copy,能应对绝大部分情况

const clone = (obj) => {
    // Handle the 3 simple types, and null or undefined
    if (obj === null || typeof obj !== 'object') return obj;
    // Handle Date
    if (obj instanceof Date) {
        const copy = new Date();
        copy.setTime(obj.getTime());
        return copy;
    }
    // Handle Array
    if (obj instanceof Array) {
        const copy = [];
        const len = obj.length;
        for (let i = 0; i < len; ++i) {
            copy[i] = clone(obj[i]);
        }
        return copy;
    }
    // Handle Object
    const copy = {};
    for (const attr in obj) {
        if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]);
    }
    return copy;
};

下一节将深入介绍:

  1. 如何处理循环引用, CircularJSON
  2. 如何处理 RegExp/Function/Null/Undefined/NaN 等类型的拷贝
  3. 如何实现一个类(含有构造方法和原型)的拷贝
  4. 经过 defineProperty 重新定义了对象, 能否进行深拷贝
  5. 如果由浅入深,从零开始写一个深拷贝的方法