Web瞎捣鼓——浅拷贝与深拷贝

今天要研究一下Object.assign()这个方法,有人说它是浅拷贝,它真的只能做浅拷贝吗?

浅拷贝?深拷贝?

浅拷贝:将源对象(或数组)的引用直接赋值给新对象(或数组),也就是它操作的是同一个内存地址。

深拷贝:将源对象(或数组)的属性值(或数组元素)复制到新对象(或数组),它操作的是一个新的内存区域。

刚刚查浅拷贝和深拷贝的概念,翻到一篇文章说:浅拷贝只复制一层对象的属性,而深拷贝则递归复制了所有层级。

阅读量3.2k,这得多少人“受益”,我的表情是这样的:

Web瞎捣鼓——浅拷贝与深拷贝 

关于深拷贝和浅拷贝,拿数组举个例子:

// 浅拷贝
let sourceArr = [1, 2, 3]
let shallowCopyArr = sourceArr

sourceArr[0] = 4
shallowCopyArr[1] = 5

console.log(JSON.stringify(sourceArr))  // [4,5,3]
console.log(JSON.stringify(shallowCopyArr))  // [4,5,3]

// 深拷贝
let deepCopyArr = [...sourceArr]

sourceArr[0] = 6
deepCopyArr[1] = 7

console.log(JSON.stringify(sourceArr))  // [6,5,3]
console.log(JSON.stringify(deepCopyArr))  // [4,7,3]

浅拷贝和深拷贝的目的很明显是截然相反的。有时我们只是需要另一个对象的引用,比如Vue的mixins混入(抠着脚丫子想了半个小时,还是没想到合适的例子)。有时我们希望在创建一个副本来进行修改而不影响源数据,比如有一组参数要传给后台,参数里有个时间需要毫秒,而时间选择器组件则依靠日期对象回显,这时传给后台的数据就需要深拷贝单独处理。

我们在开发中用到的看似都是深拷贝,于是就有人问,为什么还会有浅拷贝这个存在?当然,在数据处理方面用深拷贝的时间多一些,但在其它方面或许用到了浅拷贝却不知道。比如封装的方法对象,它是深拷贝吗?

浅拷贝就不用多说,直接赋值就是浅拷贝,需要注意的是有些地方看似深拷贝实际是浅拷贝,这个在之前的篇章中提到过。下面就来看下深拷贝。

数组的深拷贝

  • 遍历(for循环、数组迭代方法内部、其他遍历方式)
let sourceArr = [1, 2, 3]
let copyArr = []

for (let i = 0; i < sourceArr.length; i++) {
	copyArr.push(sourceArr[i])
}

sourceArr[0] = sourceArr.length * 10
copyArr[1] = copyArr.length * 10

console.log(JSON.stringify(sourceArr))  // [30,2,3]
console.log(JSON.stringify(copyArr))  // [1,30,3]
  • Array原型方法
// Array.prototype.slice() - 抽取当前数组中的一段元素组合成一个新数组。
copyArr = sourceArr.slice()

// Array.prototype.concat() - 返回一个由当前数组和其它若干个数组或者若干个非数组值组合而成的新数组。
copyArr = sourceArr.concat()

// Array.prototype.filter() - 将所有在过滤函数中返回 true 的数组元素放进一个新数组中并返回。
copyArr = sourceArr.filter(v => true)

// Array.prototype.map() - 返回一个由回调函数的返回值组成的新数组。
copyArr = sourceArr.map(v => v)
  • JSON转换
copyArr = JSON.parse(JSON.stringify(sourceArr))
  • 扩展运算符
copyArr = [...sourceArr]

以上方法部分只能拷贝单个数组,除此外,如果把数据换成复杂类型结果会怎样?

let sourceArr = [{a: 1}, {b: 2}, 3]
let copyArr = []

// 数组的深拷贝方法

sourceArr[0].a = sourceArr.length * 10
copyArr[1].b = copyArr.length * 10

最终结果是所有方法几乎没有完成任务,只有JSON转换完美搞定。接下来看看另一个数据类型的深拷贝。

对象的深拷贝

数组的深拷贝拷贝方式中有三种也适用于对象:

  • 遍历(for循环、其他遍历方式)
let sourceObj = {a: 1, b: 2}
let copyObj = {}

for (let k in sourceObj) {
	copyObj[k] = sourceObj[k]
}

console.log(JSON.stringify(sourceObj))
console.log(JSON.stringify(copyObj))

sourceObj.a = 3
copyObj.b = 4

console.log(JSON.stringify(sourceObj))  // {"a":3,"b":2}
console.log(JSON.stringify(copyObj))  // {"a":1,"b":4}
  • JSON转换
copyObj = JSON.parse(JSON.stringify(sourceObj))
  • 扩展运算符
copyObj = {...sourceObj}

除以上方法外,还有Object的方法,比如assign(),它是分情况的,后面来讨论。

copyObj = Object.assign({}, sourceObj)

接下来将数据换为复杂的数据结构:

let sourceObj = {a: [1], b: [2]}
let copyObj = {}

// 对象的深拷贝方法

sourceObj.a[0] = 3
copyObj.b[0] = 4

结果和数组一样,只有JSON转换成功的完成了深拷贝,而且对数组和对象都有效。当然通用不一定就好,合适的时候用合适的方法解决问题才是正确的选择。然而,我们通常遇到的会是这种复杂的数据结构,难道这是唯一的解决办法?从两种类型的所用方法来看,它们有三种方法是相近的,其中就有JSON转换。并且复杂的数据结构也是凭借这两种数据类型组成,那么,另外两种方式有没有办法改造成有效的深拷贝方法呢?当然,我们试一下:

// 声明:方法修改自jQuery
// 因为jQuery考虑有很多情况,所以有很多方法封装
// 本着实验的态度,直接copy了源码,我的封装没考虑变数
// 就是懒,原谅我

let class2type = {}
let toString = class2type.toString
let hasOwn = class2type.hasOwnProperty
let fnToString = hasOwn.toString
let ObjectFunctionString = fnToString.call(Object)
let getProto = Object.getPrototypeOf

// 检查是否为普通对象
function isPlainObject(obj) {
    let proto, Ctor

    if (!obj || toString.call(obj) !== ‘[object Object]‘) return false

    proto = getProto(obj)

    if (!proto) return true

    Ctor = hasOwn.call(proto, ‘constructor‘) && proto.constructor

    return typeof Ctor === ‘function‘ && fnToString.call(Ctor) === ObjectFunctionString
}

// 转换类型
function toType(obj) {
    if (obj == null) return `${obj}`

    return typeof obj === ‘object‘ ? class2type[toString.call(obj)] || ‘object‘ : typeof obj
}

// window对象判断
function isWindow(obj) {
    return obj != null && obj === obj.window
}

// 检查类数组
function isArrayLike(obj) {
    let length = !!obj && obj.length, type = toType(obj);

    if (typeof obj === "function" || isWindow(obj)) return false

    return type === "array" || length === 0 || typeof length === "number" && length > 0 && (length - 1) in obj
}

// 深拷贝封装
// 第一个参数是目标对象(数组),第二个参数为拷贝数据
// 这个方法和Object.assign()有点像,它实际是浅拷贝
// 就不去修改调整了,为后文说Object.assign()做个铺垫
function deepCopy(target, source) {
    let iterator

    if (Array.isArray(target)) {
        if (!Array.isArray(source)) {
            source = isArrayLike(source) ? Array.from(source) : [source]
        }

        iterator = source.entries()
    } else {
        if (!isPlainObject(target)) {
            target = {}
        }

        iterator = Object.entries(source)
    }

    for (let [k, v] of iterator) {
        let copy = isArrayLike(v) ? Array.from(v) : v
        let copyIsArray = Array.isArray(copy)
        let clone

        if (copy && (isPlainObject(copy) || copyIsArray)) {
            src = isArrayLike(target[k]) ? Array(target[k]) : target[k]

            if (copyIsArray && !Array.isArray(src)) {
                clone = []
            } else if (!copyIsArray && !isPlainObject(src)) {
                clone = {}
            } else {
                clone = src
            }

            copyIsArray = false

            target[k] = deepCopy(clone, copy)
        } else if (copy !== undefined) {
            target[k] = copy
        }
    }

    return target
}

Object.assign()深拷贝?浅拷贝?

前面封装了一个deepCopy()的拷贝函数,来执行一下:

let a = [1]
let b = deepCopy(a)
b[0] = 4

console.log(a, b)  // 都是[4]

let c = deepCopy([], a)
c[0] = 5

console.log(JSON.stringify(a))  // [1]
console.log(JSON.stringify(b))  // [5]

两种不同的方式,结果完全不一样,因为返回值和参数是同一个地址。Object.assign()也一样,第一个参数是目标对象,返回也是目标对象。但是第一个参数给一个新对象,就是深拷贝,这就有意思了,下面就来探索一下。前面做了许多铺垫,下面直接上代码看下Object.assign(),多给它创建几个数据,这样能更好的看出其中的变化。先来一组简单的数据类型:

let t = {}
let s1 = {i: 1}
let s2 = {i: 2}
let s3 = {i: 3}

Object.assign(t, s1, s2, s3)

t.i = 4

console.log(t, s1, s2, s3)  // 依次输出{i: 4} {i: 1} {i: 2} {i: 3}

t = Object.assign(s1, s2, s3)

t.i = 5

console.log(t, s1, s2, s3)  // 依次输出{i: 5} {i: 5} {i: 2} {i: 3}

前一种写法t对象没有影响其他对象,后一种写法s1对象被改变了。稍安勿躁,语法就是这样,其他参数对象的数据拷贝给第一个参数s1对象,返回s1对象给t。就像deepCopy()第二种写法,就和前一种写法结果一样了:

t = Object.assign({}, s1, s2, s3)

看来简单数据一切正常,这个时候它是深拷贝的,换成复杂的数据类型再看看:

let t = {}
let s4 = { i: 4, d: { n: 4 } }
let s5 = { i: 5, d: { n: 5 } }
let s6 = { i: 6, d: { n: 6 } }

Object.assign(t, s4, s5, s6)

t.i = 7
t.d.n = 7

console.log(t, s4, s5, s6)
// 依次输出:
// {i: 7, d: { n: 7 }}
// {i: 4, d: { n: 4 }}
// {i: 5, d: { n: 5 }}
// {i: 6, d: { n: 7 }}

这下问题来了,所有对象的简单数据类型都没受影响,object类型受到影响,对象s6的d属性被改变。这充分说明了这个方法是浅拷贝,深拷贝也就是对于这样的复杂数据结构。换个说法就是只拷贝了指针,第一次拷贝了s4的,第二次拷贝了s5,第三次拷贝了s6。所以,对象t对d属性进行修改,s6也被改变。

换成另一种写法,对象t作为返回值也一样,对象t和第一个参数对象一样,最后一个参数对象的属性d被改变。

总结

事实证明Object.assign()确实是浅拷贝,只能用它来合并对象(哈哈,我终于找到浅拷贝的用处了)。它和我的deepCopy()还是有差别的,那么我的deepCopy()问题出在哪儿呢?当只传一个参数的时候,就是将第一个参数直接返回给了新变量,是浅拷贝。(顺便说一下,这个函数封装用的是循环,扩展运算符是不是也可以封装深拷贝呢?我认为是可以的。)

在日常开发中一定要注意许多原生方法它执行的是深拷贝还是浅拷贝,不然神不知鬼不觉地做了错误的改变。我就犯过这样的低级错误:数据显示的是元,存数据库的是分,我用数组的map()方法进行遍历,回调函数带出的每个元素值用一个变量接收,将修改之后的值返回产生新数组。不幸的是数组存储的是对象,回调函数带出的参数只是一个引用,一不小心就将源数据给改了。改变数据的同时还造了一些废代码出来,得不偿失呀!

来源:站长资讯中心

Web瞎捣鼓——浅拷贝与深拷贝

上一篇:jquery给动态生成的元素绑定事件,on函数


下一篇:angular 引入本地js