js对象数组深浅拷贝总结

为什么有深浅拷贝

这得从JavaScript的变量中包含两种类型的值说起

  1. 基本类型值

    基本类型值指的是存储在栈中的一些互相隔离的简单的数据段,比如 String, Number, Boolean 等简单类型

  2. 引用类型值

    引用类型值是引用类型的实例,它是保存在堆内存中的一个对象,引用类型是一种数据结构,最常用的是Object, Array, Function类型,另外还有Date, RegExp, Error等,es6同样也提供了Set, Map新的数据结构

    例如:

    var obj = {a: 1}
    var obj1 = obj
    obj === obj1	// true
    obj1.a = 2
    obj.a			// 也变成了2
    

    在实际内存中发生了:
    复制对象时内存发生的变化

    这种情况下,修改了Obj1,就会修改到Obj,于是就有了“拷贝”之说。

1. 浅拷贝

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

也就说浅拷贝是将两个变量进行异化,但未改变内部可能还存在的引用类型。个人总结浅拷贝有以下几种方式,适用于对象和数组:

1.1 遍历拷贝

function cloneByTraverse(obj) {
    let newObj = Array.isArray(obj) ? [] : {}
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = obj[key]
        }
    }
    return newObj
}

特点:“暴力”枚举第一层属性进行浅拷贝

1.2 Object.assign

function cloneByAssign(obj) {
    return Object.assign(Array.isArray(obj) ? [] : {}, obj)
}

特点:代码简洁,易实现,并且:

  1. 不会拷贝对象继承的属性
  2. 不可枚举的属性
  3. 属性的数据属性/访问器属性
  4. 可以拷贝Symbol类型

1.3 ES6的对象展开

function cloneByExpand(obj) {
    return Array.isArray(obj) ? [...obj] : {...obj}
}

特点:代码简洁,易实现,和Object.assign有一样的问题,只支持 e6 及以上的语法

1.4 Array.slice 或 Array.from

这个只针对数组

function cloneBySlice(arr) {
    return arr.slice()
}
function cloneByFrom(arr) {
    return Array.from(arr)
}

特点:利用了纯净函数不改变参数返回新的内容的特点,事实上也是间接利用了 slice 或 from 中的拷贝,只支持 e6 及以上的语法

2. 深拷贝

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

简单来说就是复制一份完全一样和隔离的对象。深拷贝主要要通过逐层递归遍历去实现了:

2.1 JSON 处理

function cloneByJSON(obj) {
    return JSON.parse(JSON.stringify(obj))
}

特点:利用了JSON一进一出,实现拷贝复制,但缺点还是比较多的:

  1. 拷贝的对象的值中如果有函数,undefined,symbol则经过JSON.stringify()序列化后的JSON字符串中这个键值对会消失
  2. 无法拷贝不可枚举的属性,无法拷贝对象的原型链
  3. 拷贝Date引用类型会变成字符串
  4. 拷贝RegExp引用类型会变成空对象
  5. 对象中含有NaN、Infinity和-Infinity,则序列化的结果会变成null
  6. 当对象中存在循环引用,例如obj[key] = obj,会出错

2.2 遍历拷贝

function deepCloneByTraverse(obj) {
    let cloneObj = Array.isArray(obj) ? [] : {}
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            if (typeof obj[key] === 'object') {
                //值是对象就再次调用函数
                cloneObj[key] = deepCloneByTraverse(obj[key])
            } else {
                //基本类型直接复制值
                cloneObj[key] = obj[key]
            }   
        }
    }
    return cloneObj 
}

特点:逐层扫描,如果是引用类型,则递归,否则直接复制,但还是存在以下缺点:

  1. 并不能复制不可枚举的属性以及Symbol类型
  2. 这里只是针对Object引用类型的值做的循环迭代,而对于Array,Date,RegExp,Error,Function引用类型无法正确拷贝
  3. 对象循环引用成环了的情况

因此,进行优化如下:

function deepClone(obj, hash = new WeakMap()) {
    if (obj.constructor === Date)
        return new Date(obj)   //日期对象就返回一个新的日期对象
    if (obj.constructor === RegExp)
        return new RegExp(obj)  //正则对象就返回一个新的正则对象
    //如果成环了,参数obj = obj.loop = 最初的obj 会在WeakMap中找到第一次放入的obj提前返回第一次放入WeakMap的cloneObj
    if (hash.has(obj))
        return hash.get(obj)

    let allDesc = Object.getOwnPropertyDescriptors(obj)     //遍历传入参数所有键的特性
    let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc) //继承原型链

    hash.set(obj, cloneObj)

    for (let key of Reflect.ownKeys(obj)) {   //Reflect.ownKeys(obj)可以拷贝不可枚举属性和符号类型
        // 如果值是引用类型(非函数)则递归调用deepClone
        cloneObj[key] =
            (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ?
                deepClone(obj[key], hash) : obj[key];
    }
    return cloneObj
}

const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
  1. 利用Reflect.ownKeys()方法,能够遍历对象的不可枚举属性和Symbol类型
  2. 当参数为Date,RegExp类型则直接生成一个新的实例
  3. 使用Object.getOwnPropertyDescriptors()获得对象的所有属性对应的特性,结合Object.create()创建一个新对象继承传入原对象的原型链
  4. 利用WeekMap()类型作为哈希表,WeekMap()因为是弱引用的可以有效的防止内存泄露,作为检测循环引用很有帮助,如果存在循环引用直接返回WeekMap()存储的值
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值