为什么要对js的对象进行拷贝?

var a = { a: 'a' }
var b = a
b.a = 'b'
a.a // 'b'

因为js中的对象(引用数据类型)保存在堆中,而多个变量引用同一块堆中的内容时,其中一个修改,则会影响所有引用改内容的变量;有时候我们不希望这样

实现拷贝有什么办法?

  • 方法一

浅拷贝,对目标对象进行遍历,然后将其属性逐一拷贝至新建对象中

function shallowCopy(source) {
    const obj = {}
    for (let i in source) {
        obj[i] = source[i]
    }
    return obj
}

测试一下

var obj = {
    a: 1,
    b: [1, 2, 3],
    c: {c1: 4}
}

var obj1 = shallowCopy(obj)
obj1.a = 11
obj1.b[0] = 11
obj1.c.c1 = 44
obj.a // 1
obj.b[0] // 11
obj.c.c1 // 44

测试结果
浅拷贝适用于对象中所有属性都为基础数据类型时,如果源对象包含有引用类型的属性,则浅拷贝无法切断与源对象的关系
es6提供了Object.assign,功能与之类似
还有展开运算符: var obj1 = {...obj}

  • 方法二

针对方法一的问题,提出新的方法:
利用JSON.stringify + JSON.parse 进行对象的序列化/反序列化进行对象的拷贝

var obj1 = JSON.parse(JSON.stringify(obj))
obj1.a = 11
obj1.b[0] = 11
obj1.c.c1 = 44
obj.a // 1
obj.b[0] // 1
obj.c.c1 // 4

方法二似乎完美得解决了方法一遗留的问题,再进行测试

function Person(age) {
    this.age = age
}
var p = new Person(3)
var obj = {
    a: new Date(),
    b: /abc/igmuy,
    c() {},
    d: new Array(2),
    e: p,
}
var obj1 = JSON.parse(JSON.stringify(obj))
obj1.a // '2019-12-18T08:40:21.650Z' 实际上和调用Date对象的toISOString方法一致,返回的为时间字符串
obj1.b // {} RegExp对象不会被正确拷贝
obj1.c // undefined 函数不会被正确拷贝
obj1.d // [null, null] 数组空位不会被正确拷贝
obj1.e // obj1.e.constructor指向Object,即原型丢失

还有一种情况,包含循环引用过的对象使用JSON序列化会抛出异常

var obj = {}
obj.a = obj
var obj1 = JSON.parse(JSON.stringify(obj))
// 结果会抛出异常

使用JSON序列化/反序列化实现拷贝的局限性

  1. 无法对函数、RegExp、Date等特殊对象进行拷贝
  2. 拷贝稀疏数组时会拷贝相同长度但以null填充的数组
  3. 循环引用对象使用该方法会导致异常
  4. 对对象属性进行拷贝时会丢失对象的原型链

以上几点虽然是该方法的局限,但通常该方法能满足大多数场景,也不失为一个简便的方法

  • 方法三

针对方法一无法对引用类型属性进行拷贝和方法二存在的局限,可以针对不同情况,进行针对性处理

// 类型判断方法
const isType = (source, type) => Object.prototype.toString.call(source).slice(8, -1).toLowerCase() === type.toLowerCase()

const deepClone = source => {
    const sources = []
    const children = []
    const _deep = source => {
        if (typeof source !== 'object' ||
            isType(source, 'null') ||
            isType(source, 'undefined')
            ) {return source}
        let child;
        if (isType(source, 'date')) { // 处理date类型
            child = new Date(source.getTime())
        }
        else if (isType(source, 'regexp')) { // 处理正则
            child = new RegExp(source.source, source.flags)
            child.lastIndex = source.lastIndex
        }
        else if (isType(source, 'array')) { // 处理数组
            child = []
        }
        
        else {
            const proto = Object.getPrototypeOf(source)
            child = Object.create(proto)
        }
        
        // 处理循环引用
        const index = sources.indexOf(source)
        if (index >= 0) {
            return children[index]
        }
        
        sources.push(source)
        children.push(child)
        
        for (let i in source) {
            child[i] = _deep(source[i])
        }
        return child
    }
    return _deep(source)
}

该方法解决了前两个方法中存在的一些问题

  • 方法四

虽然方法三解决了一些问题,但同时引入了一些问题: 由于使用了遍历递归的方法,在性能方面会有一些问题,有没有办法优化呢?

。。。


innocence
11 声望1 粉丝

undefined