工具函数系列-浅拷贝与深拷贝

参考《你不知道的JavaScript(中卷)》第2章28页

引用就像一种特殊的指针,是来指向变量的指针(别名)。如果参数不声明为引用的话,参数值总是通过值复制的方式传递,即便对复杂的对象值也是如此。

JavaScript 中没有指针,引用的工作机制也不尽相同。在 JavaScript 中变量不可能成为指向另一个变量的引用。

JavaScript 引用指向的是值。如果一个值有 10 个引用,这些引用指向的都是同一个值, 它们相互之间没有引用 / 指向关系。

JavaScript 对值和引用的赋值 / 传递在语法上没有区别,完全根据值的类型来决定。

简单值(即标量基本类型值, scalar primitive) 总是通过值复制的方式来赋值 / 传递,包括null、 undefined、字符串、数字、布尔和 ES6 中的 symbol。

复合值(compound value)——对象(包括数组和封装对象,参见第 3 章)和函数,则总是通过引用复制的方式来赋值 / 传递。

总之,简单标量基本类型值(字符串和数字等)通过值复制来赋值 / 传递,而复合值(对象等)通过引用复制来赋值 / 传递。 JavaScript 中的引用和其他语言中的引用 / 指针不同,它们不能指向别的变量 / 引用,只能指向值。

基础类型存储在内存的栈中,而引用类型存储在内存的堆中。

浅拷贝和深拷贝的含义

浅拷贝是指值的引用的复制,并不是真正的值的复制。即只是复制了指向值的指针,但是指向的位置还是在原来的内存地址上。一旦原值改变就会引发所有的指向这个值的引用结果发生改变。

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

基本数据类型如String、Number等会进行值的拷贝,引用数据类型会进行值的引用的拷贝。

只有将对象中的基本数据类型存在新对象的地址中,才是真正的拷贝。

总之,浅拷贝就是两个引用共享同一块内存地址,深拷贝就是两个引用指向值相同但位置不同的两个独立的对象。

浅拷贝

对象和数组的拷贝仅支持一层的拷贝,即如果属性值或数组元素是引用类型,拷贝出来的对象属性值和数组元素也是对引用类型指针的复制。

对象的浅拷贝,要进行区分对象属性值是基本数据类型还是引用数据类型。数组的浅拷贝,要进行区分元素值是基本数据类型还是引用数据类型。

这里指的浅拷贝仅仅表示对象或数组的属性值或元素是基础数据类型

对象的浅拷贝

赋值
var a = {x: 1}
var b = a
a.x = 2
console.log(b.x) // 2
Object.assign
var a = {x: 1}
var b = Object.assign(a, {
    x: 2
})
console.log(a.x) // 2
a.x = 3
console.log(b.x) // 3

数组的浅拷贝

数组的拷贝

赋值
var a = [1, 2, 3, 4, 5]
var b = a
a[0] = 0
console.log(b[0]) // 0

深拷贝

对象

这里指的深拷贝仅仅表示对象的属性值是基础数据类型

对象的函数只要返回新对象的方式,都可以作为对象深拷贝的方式。

展开运算符...
var a = {x: 1}
var b = {...a}
a.x = 3
console.log(b.x) // 1

数组

这里指的深拷贝仅仅表示数组的元素值是基础数据类型

数组的函数只要返回新数组的方式,都可以作为数组深拷贝的方式。

展开运算符...
var a = [1, 2, 3, 4, 5]
var b = [...a]
a[0] = 0
console.log(b[0]) // 1
Array.prototype.concat()
// 方法一
var a = [1, 2, 3, 4, 5]
var b = [].concat(a)
a[0] = 0
console.log(b[0]) // 1

// 方法二
var a = [1, 2, 3, 4, 5]
var b = a.concat()
a[0] = 0
console.log(b[0]) // 1
Array.prototype.slice()
var a = [1, 2, 3, 4, 5]
var b = a.slice()
a[0] = 0
console.log(b[0]) // 1

特殊对象和对象数组

这里指的深拷贝表示对象和数组的属性值和元素值有引用数据类型的情况

JSON

使用JSON.stringify(obj)将对象转换为JSON字符串之后,再使用JSON.parse(str)JSON字符串转换为对象。

JSON.stringify(obj)将对象转换为JSON字符串之后会将字符串存到另外一个内存地址上,JSON.parse(str)JSON字符串转换为对象后就是将这个对象存到另一个内存地址上,这样新生成的对象和旧对象的内存地址不同,实现了深拷贝。

JSON.parse(JSON.stringify(obj))

但是需要注意,JSON.stringify()会忽略undefinedSymbol()function

Date会进行value转换

console.log(JSON.stringify({ b: null, c: undefined, d: 1, e: 'str', f: function() {}, g: Symbol(), i: true }))
// expected output: "{"b":null,"d":1,"e":"str","i":true}"

console.log(JSON.stringify({ x: [10, undefined, function(){}, Symbol('')] }));
// expected output: "{"x":[10,null,null,null]}"

console.log(JSON.stringify(new Date(2006, 0, 2, 15, 4, 5)));
// expected output: ""2006-01-02T15:04:05.000Z""

所以如果有基本类型外的其他特殊的数据类型需要进行自己进行手动深拷贝。

递归

参考关于JS数据类型检测

function deepClone(obj) {
    if (obj === null || obj === undefined) {
        return obj
    }
    if (obj instanceof Date) {
        return new Date(obj)
    }
    if (obj instanceof RegExp) {
        return new RegExp(obj)
    }
    if (obj instanceof Function) {
        return obj
    }
    if (obj instanceof Array) {
        let newArr = [...obj] // 基础类型数据先进行拷贝
        newArr = newArr.map(item => deepClone(item)) // 拷贝引用类型
        return newArr
    }
    if (obj instanceof Object) {
        let newObj = {}
        for (let key in obj) {
            if (obj.hasOwnProperty(key)) { // 排除原型链上的属性,仅循环当前对象上的属性
                newObj[key] = deepClone(obj[key])
            }
        }
        return newObj
    }
    return obj // 基础类型直接返回 String, Number, Boolean, Symbol
}

// 测试
var x = {
    a: 1,
    b: 'str',
    c: true,
    d: Symbol('x'),
    e: null,
    f: undefined,
    g: function() { return 'g' },
    h: {
        m: 1,
        n() {
            return 'n'
        }
    },
    j: [1, 2, 3],
    k: [[4, 5], [6, 7]],
    l: [{
        m: 1,
        n: 2
    }, {
        m: 3,
        n: 4
    }]
}

var y = deepClone(x)
x.a = 2
x.d = Symbol('x2')
x.g = function() { return 'g2' }
x.h.m = 2
x.h.n = function() { return 'n2' }
x.k[0] = [0, 1]
x.k[1][0] = 8
x.l[0].m = 11

var log = console.log
log(y)
log(y.g())
log(y.h.n())
// 	{
//     a: 1,
//     b: 'str',
//     c: true,
//     d: Symbol(x),
//     e: null,
//     f: undefined,
//     g: [Function: g],
//     h: { m: 1, n: [Function: n] },
//     j: [ 1, 2, 3 ],
//     k: [ [ 4, 5 ], [ 6, 7 ] ],
//     l: [ { m: 1, n: 2 }, { m: 3, n: 4 } ]
//   }
//   g
//   n
优化

对象需要考虑是否是循环引用的情况

关于Symbol作为属性名:Symbol 作为属性名,遍历对象的时候,该属性不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。

如果需要获取到对象中的Symbol属性,则需要调用Object.getOwnPropertySymbols()获取。

// 优化Symbol部分
if (obj instanceof Object) {
    let newObj = {}
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) { // 排除原型链上的属性,仅循环当前对象上的属性
            newObj[key] = deepClone(obj[key])
        }
    }
    const symbols = Object.getOwnPropertySymbols(obj) // 获取Symbol属性的值
    // 拷贝symbol属性的值
    symbols.forEach(key => {
        newObj[key] = obj[key]
    })
    return newObj
}

其他库中的实现

Vuex源码

Vuex 源码中的深拷贝方法用于其自带的 logger plugin。

会检查数据中是否有环的存在,进行递归拷贝。

/**
 * Deep copy the given object considering circular structure.
 * This function caches all nested objects and its copies.
 * If it detects circular structure, use cached copy to avoid infinite loop.
 *
 * @param {*} obj
 * @param {Array<Object>} cache
 * @return {*}
 */
export function deepCopy (obj, cache = []) {
  // just return if obj is immutable value
  if (obj === null || typeof obj !== 'object') {
    return obj
  }

  // if obj is hit, it is in circular structure
  const hit = find(cache, c => c.original === obj)
  if (hit) {
    return hit.copy
  }

  const copy = Array.isArray(obj) ? [] : {}
  // put the copy into cache at first
  // because we want to refer it in recursive deepCopy
  cache.push({
    original: obj,
    copy
  })

  Object.keys(obj).forEach(key => {
    copy[key] = deepCopy(obj[key], cache)
  })

  return copy
}

参考链接

浅拷贝与深拷贝 – 掘金