JavaScript的对象拷贝

121 阅读4分钟

总结自若离的《JavaScript核心原理精讲》

在JavaScript编程中,经常会遇到拷贝对象的情况,在拷贝的场景中,存在着两种情况,所谓的浅拷贝与深拷贝。

浅拷贝

浅拷贝意味着将与一个对象的所有属性拷贝到一个新的对象上。浅的意思是只拷贝对象的属性而不考虑该属性值是否是引用类型(即嵌套情况)。新的对象虽然开辟了一个新的内存地址,但该地址引用的对象属性都是原来对象的属性。

浅拷贝的实现

Object.assign

Object.assign具有拷贝对象的能力。 本来该方法的用法是将一个对象与另一个对象进行合并const res = Object.assign(target, source)。因此当target为空对象时,即实现了浅拷贝的功能。

  • 该方法会将源对象上的可枚举属性与自有属性复制到目标对象上(包括Symbol类型的属性),不会拷贝继承的属性。
  • 该方法会将对象中的访问器属性转换为数据属性复制到目标对象中,因为该方法使用源对象的[[Get]]来获取属性值,用目标对象上的[[Set]]来设置属性值

扩展运算符

const res = { ...source } 与Object.assign的拷贝类似

数组的concat拷贝和slice拷贝

const res = [1, { a: 2 }, 3].concat()

const res = [1, 2, { a: 3 }, 4].slice(1, 3)

本质上讲,数组同样也是对象,而concat方法和slice方法是数组实例的方法,最终结果也是浅拷贝。

实现

function shallowCopy(source) {
    const target = Array.isArray(source)? []: {}
    for(let prop in source) {
        if (souce.hasOwnProperty(prop)) {
            target[prop] = source[prop]
        }
    }
    return target
}

深拷贝

与浅拷贝相对的是深拷贝,深拷贝意味着新建一个目标对象(内存区域),将源对象从内存中完整拷贝出来给目标对象,这样实现了源对象与目标对象的彻底分离,双方互不影响。

JSON方法

先将源对象序列化const mid = JSON.stringify(source)

再将序列化的数据解析为新的对象即可const res = JSON.parse(mid)

  • 因为JSON方法是用来做序列化的,因此无法拷贝属性值是函数、undefined和Symbol属性
  • 无法拷贝循环引用obj.key = obj
  • 拷贝Date引用类型时属性值会变成字符串
  • 无法拷贝不可枚举属性
  • 无法拷贝原型链
  • 拷贝正则类型会变成空对象
  • 拷贝NaN、Infinity、-Infinity时,序列化后变为null

如果深拷贝只是为了数据的拷贝,那么使用JSON方法无疑是最快捷最实用的方式。

基本版递归实现

function deepClone(source) {
    const target = Array.isArray(source)? []: {}
    for(let prop in source) {
        if (typeof source[prop] === 'object') {
           target[prop] = deepClone(source[prop]) 
        } else {
            target[prop] = source[prop]
        }
    }
    return target
}
  • 无法拷贝方法、不可枚举属性和Symbol属性
  • 只是普通的引用类型值的拷贝
  • 无法解决循环引用(陷入死循环)

改进版递归实现

  • 针对不可枚举属性和Symbol类型值,实用Reflect.ownKeys方法
    • Reflect.ownKeys(obj)返回obj对象自身属性名组成的数组,无论是否可枚举,并且包括Symbol类型的属性名
  • 当参数source是一个Date类型或者正则类型时,则直接生成一个新的实例返回
  • 利用Object.getOwnPropertyDescriptors(obj)方法可以获得对象的所有自有属性及其属性描述对象,结合Object.create(proto)方法创建新对象,并继承传入源对象的原型链
  • 利用WeakMap类型作为Hash表,检测是否存在循环引用。如果存在循环引用,则引用直接返回WeakMap存储的值
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)

const deepClone = function (obj, hash = new WeakMap()) {

  if (obj.constructor === Date) 

  return new Date(obj)       // 日期对象直接返回一个新的日期对象

  if (obj.constructor === RegExp)

  return new RegExp(obj)     //正则对象直接返回一个新的正则对象

  //如果循环引用了就用 weakMap 来解决,我们只是用WeakMap来存储一下obj 与 cloneObj的映射关系,个人觉得hash在函数执行完后就释放了,没必要非用WeakMap来处理

  if (hash.has(obj)) return hash.get(obj)

  let allDesc = Object.getOwnPropertyDescriptors(obj)

  //继承原型链并将obj的自有属性添加到cloneObj上
  let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)

  hash.set(obj, cloneObj)
  
  //遍历传入参数所有键的特性
  for (let key of Reflect.ownKeys(obj)) { 

    cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]

  }

  return cloneObj

}

// 下面是验证代码

let obj = {

  num: 0,

  str: '',

  boolean: true,

  unf: undefined,

  nul: null,

  obj: { name: '我是一个对象', id: 1 },

  arr: [0, 1, 2],

  func: function () { console.log('我是一个函数') },

  date: new Date(0),

  reg: new RegExp('/我是一个正则/ig'),

  [Symbol('1')]: 1,

};

Object.defineProperty(obj, 'innumerable', {

  enumerable: false, value: '不可枚举属性' }

);

obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))

obj.loop = obj    // 设置loop成循环引用的属性

let cloneObj = deepClone(obj)

cloneObj.arr.push(4)

console.log('obj', obj)

console.log('cloneObj', cloneObj)