JavaScript深拷贝探究

JavaScript深拷贝

1、JavaScript数据类型

​ JavaScript数据类型分为两类:基本数据类型(也被称为原始类型 primitive type)和对象类型(也被称为引用数据类型 object type)

  • 其中基本数据类型包括:null、boolean、undefined、string、number、symbol
  • 引用数据类型包括:Array、Object、Function、Date等

2、javascript的变量的存储方式–栈(stack)和堆(heap)

  • 栈(stack):自动分配内存空间,系统自动释放,里面存放的是基本类型的值和引用类型的地址
  • 堆(heap):动态分配的内存,大小不定,也不会自动释放。里面存放引用类型的值。

堆栈示例

3、javascript值传递与址传递

​ 基本类型与引用类型最大的区别实际就是传值与传址的区别

​ 值传递:基本类型采用的是值传递。

​ 址传递:引用类型则是地址传递,将存放在栈内存中的地址赋值给接收的变量。

4、深拷贝与浅拷贝

  • 浅拷贝:浅拷贝是拷贝引用,拷贝后的引用都是指向同一个对象的实例,彼此之间的操作会互相影响
  • 深拷贝:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响

5、JavaScript中实现一个深拷贝

  • 第一种方法:使用JSON解析解决

    1
    2
    3
    4
    5
    let a = {
    x:1,
    y:[1,2,4]
    }
    let b = JSON.parse(JSON.stringify(a));
  • 第二种方法:使用递归解析解决

    1
    2
    3
    4
    5
    6
    js中遍历一个对象的属性的方法:
    Object.keys() 仅仅返回自身的可枚举属性,不包括继承来的,更不包括Symbol属性
    Object.getOwnPropertyNames() 返回自身的可枚举和不可枚举属性。但是不包括Symbol属性
    Object.getOwnPropertySymbols() 返回自身的Symol属性
    for...in 可以遍历对象的自身的和继承的可枚举属性,不包含Symbol属性
    Reflect.ownkeys() 返回对象自身的所有属性,不管是否可枚举,也不管是否是Symbol。注意不包括继承的属性

    经过查阅资料,自己整理思考之后,初步编写了一个简单的深拷贝函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    function deepClone (obj) {
    // 如果是简单数据类型则直接返回数据
    if (isPrimitive(obj)) {
    return obj
    }
    // 判断拷贝对象是否是数组
    let newObj = Array.isArray(obj) ? [] : {};
    // 使用Reflect.ownKeys可以获取包括Symbol属性的所有属性的数组
    Reflect.ownKeys(obj).forEach(i => {
    if (typeof obj[i] === 'object') {
    // 递归处理
    newObj[i] = deepClone(obj[i]);
    } else {
    // 简单处理 Function的情况
    newObj[i] = obj[i];
    }
    });
    return newObj;
    }

    function isPrimitive(value){
    return (typeof value === 'string' ||
    typeof value === 'number' ||
    typeof value === 'symbol' ||
    typeof value === 'boolean'||
    typeof value === 'undefined' ||
    value === null)
    }

    经过测试该方法可以处理常见的大多数的深拷贝问题(obj可以是嵌套的复杂json对象,包括Array,Object、Function、Symbol)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    let s1 = Symbol("first");
    let xm = {
    name: '小明',
    age:18,
    lessons:['数学','语文','英语'],
    brothers:[{
    name:"大明",
    age:19
    },{
    name:"小小明",
    age:17
    }],
    girlFriends:null,
    son:undefined,
    money: NaN,
    arr: new Object({a:1}),
    [s1]:'test',
    say: function () {
    console.log('-----1-----',)
    }
    };

    console.log(deepClone(xm)) // 结果正确!

    但是在测试以及学习其他优秀的库( lodash )是如何实现深拷贝的过程中也发现了一些问题。针对目前自己编写的这个深拷贝函数,

    1、对于JavaScript其他内置数据类型的支持不够。比如Date、Set、ArrayBuffer等,lodash使用Object.prototype.toString.call()方法处理了几乎所有的数据类型。如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    /** `Object#toString` result references. */
    var argsTag = '[object Arguments]',
    arrayTag = '[object Array]',
    boolTag = '[object Boolean]',
    dateTag = '[object Date]',
    errorTag = '[object Error]',
    funcTag = '[object Function]',
    genTag = '[object GeneratorFunction]',
    mapTag = '[object Map]',
    numberTag = '[object Number]',
    objectTag = '[object Object]',
    regexpTag = '[object RegExp]',
    setTag = '[object Set]',
    stringTag = '[object String]',
    symbolTag = '[object Symbol]',
    weakMapTag = '[object WeakMap]';

    var arrayBufferTag = '[object ArrayBuffer]',
    dataViewTag = '[object DataView]',
    float32Tag = '[object Float32Array]',
    float64Tag = '[object Float64Array]',
    int8Tag = '[object Int8Array]',
    int16Tag = '[object Int16Array]',
    int32Tag = '[object Int32Array]',
    uint8Tag = '[object Uint8Array]',
    uint8ClampedTag = '[object Uint8ClampedArray]',
    uint16Tag = '[object Uint16Array]',
    uint32Tag = '[object Uint32Array]';

    需要完善这个问题就是一个精细的活儿了,之后有时间想折腾了会考虑参照lodash的处理方式,使用更精确的类型判断方法来修改这个深拷贝的函数。当然在生产环境中还是推荐大家使用lodash这种成熟的库好(^-^)。

    2、对于循环引用的问题没有处理,遇见循环应用的问题时会内存溢出、爆栈。

    1
    2
    3
    4
    let a = {};
    let b = {a};
    a.b = b;
    console.log(deepClone2(a)) // RangeError: Maximum call stack size exceeded

    其中lodash使用了一个stack来解决了这个问题

    1
    2
    3
    4
    5
    6
    7
    // Check for circular references and return its corresponding clone.
    stack || (stack = new Stack);
    var stacked = stack.get(value);
    if (stacked) {
    return stacked;
    }
    stack.set(value, result);

    我们也可以使用类似的思想来解决循环引用的问题。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    function deepClone (obj) {
    let stack = {};
    function baseClone () {
    if (isPrimitive(obj)) {
    return obj;
    }
    let newObj = Array.isArray(obj) ? [] : {};
    Reflect.ownKeys(obj).forEach(i => {
    if (typeof obj[i] === 'object') {
    // 增加了记录被拷贝过的引用地址。以此来解决循环引用的问题
    if (stack[obj[i]]) {
    newObj[i] = stack[obj[i]]
    } else {
    stack[obj[i]] = obj[i];
    newObj[i] = baseClone(obj[i]);
    }

    } else {
    newObj[i] = obj[i];
    }
    });
    return newObj;
    }
    return baseClone(obj)
    }

参考

javascript中的深拷贝和浅拷贝?

JavaScript|MDN