jannahuang / blog

MIT License
0 stars 0 forks source link

对象的拷贝和深拷贝 #19

Open jannahuang opened 2 years ago

jannahuang commented 2 years ago

对象的引用和复制

赋值了对象的变量存储的不是对象本身,而是该对象“在内存中的地址”,即对该对象的“引用”。当我们对对象执行操作时,例如获取一个属性,JavaScript 引擎会查看该地址中的内容,并在实际对象上执行操作。 当一个对象变量被复制时,只是该引用被复制,而该对象自身并没有被复制。

let user = { name: "John" };
let admin = user; // 复制引用

这时有了两个变量,它们保存的都是对同一个对象的引用。 (图 copy-object)

仅当两个变量都引用同一个对象,它们才相等。

克隆与合并,Object.assign(浅拷贝)

创建一个新对象,通过遍历已有对象的属性,并在原始类型值的层面复制它们,以实现对已有对象结构的复制。

let user = {
  name: "John",
  age: 30
};

let clone = {}; // 新的空对象

// 将 user 中所有的属性拷贝到其中
for (let key in user) {
  clone[key] = user[key];
}

或者用语法 Object.assign(dest, [src1, src2, src3...]) 代替上述 for 循环。

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

let clone = Object.assign({}, user);

Object.assign 方法只会拷贝源对象可枚举的自身的属性到目标对象。如果目标对象与源对象具有相同的属性,则目标对象中的属性将被源对象中的属性覆盖。

深层克隆(深拷贝)

而当属性不为原始类型,而是对象时,用 Object.assign() 方法克隆对象则是以引用形式被拷贝。 为了能复制出两个独立的对象,应该使用一个拷贝循环来检查源对象的每个值,如果它是一个对象,则复制它的结构。这就是所谓的“深拷贝”。 可以用递归方式实现深拷贝,或者采用现有的实现,例如 lodash 库的 _.cloneDeep(obj)。

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

实现深拷贝 方法1:JSON

const b = JSON.parse(JSON.stringify(a))

不建议使用该方法,该方法有如下缺点:

  1. 不支持 Date、正则、undefined、函数等数据
  2. 不支持引用(即环状结构)

实现深拷贝 方法2:递归

1. 区分数据类型 JavaScript 有 8 种数据类型:string number bool null undefined symbol bigint object。 在拷贝数据时,要判断数据类型,object 的情况较多需单独处理,其它基本数据类型可直接返回。

function deepClone(target) {
  if (typeof target === 'object') {
    let result = {}
    for (let key in target) {
      result[key] = deepClone(target[key])
    }
    return result
  } else {
    return target
  }
};

2. 兼容数组 考虑数组的情况,改写如下:

function deepClone(target) {
  if (typeof target === 'object') {
    let result = Array.isArray(target) ? [] : {} //区分数组和对象
    for (let key in target) {
      result[key] = deepClone(target[key])
    }
    return result
  } else {
    return target
  }
}

3. 解决循环引用 如果使 target.self = target,在拷贝时,则会出现循环引用,导致死循环。 可以用 Map 来存储当前对象和拷贝对象的对应关系,如果已经拷贝过当前对象,则直接返回,如果没有,则继续拷贝。

function deepClone(target, cache) {
  if(!cache) {
    cache = new Map() //如果没有缓存,则创建
  }
  if (typeof target === 'object') {
    let result = Array.isArray(target) ? [] : {}
    if (cache.get(target)) {
      return cache.get(target)
    }
    cache.set(target, result)
    for (const key in target) {
      result[key] = deepClone(target[key], cache) //递归调用时传递缓存
    }
    return result
  } else {
    return target
  }
}

然而 Map 会形成对于对象的引用,而不再需要所引用的对象时,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放所引用的对象占用的内存,而造成内存泄漏。 为了解决这个问题,ES6 新增了 WeakMap 数据结构。 WeakMap与Map的区别有两点:

4. 性能优化,用 while 替代 for...in 经过测试可知,while 的速度比 for...in 快,用 while 可提升代码效率。

function deepClone(target, cache) {
  if(!cache) {
    cache = new Map() //如果没有缓存,则创建
  }
  if (typeof target === 'object') {
    let isArray = Array.isArray(target)
    let result = isArray ? [] : {}
    if (cache.get(target)) {
      return cache.get(target)
    }
    cache.set(target, result)

    // 用 while 替代 for...in
    let index = -1
    let keys = isArray ? undefined : Object.keys(target)
    let array = keys || target
    const length = keys ? keys.length : target.length
    while (++index < length) {
      let key = index
      let value = array[index]
      if (keys) {
        key = value
      }
      result[key] = deepClone(target[key], cache)
    }

    return result
  } else {
    return target
  }
}

5. 处理其它数据类型 用 typeof 判断类型的返回值中,null 类型的结果是 "object",function 对象的结果是 function,判断对象是否为引用类型时要区分这两种情况。

// 特殊处理 null 情况,function 也是一种对象,先当作对象类型返回
function isObject(target) {
  const type = typeof target;
  return target !== null && (type === 'object' || type === 'function')
}

然后用 toString() 方法获取准确的引用类型:

function getType(target) {
  return Object.prototype.toString.call(target)
}

常用的类型 这些类型里又可以分为两类:

可以继续遍历的类型 由于类型较多,选取部分进行处理,比如 object,array,Map,Set 都属于可以继续遍历类型。 首先需要获取它们的初始化数据,通过该对象的 constructor 来获取。这种方法的好处是:使用原对象的构造方法,可以保留对象原型上的数据。

function getInit(target) {
  const Ctor = target.constructor
  return new Ctor()
}

处理可继续遍历类型:

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
// 可以继续遍历的类型
const deepTag = [mapTag, setTag, arrayTag, objectTag]

function deepClone(target, cache) {
  // 直接返回原始数据类型
  if(!isObject(target)) {
    return target
  }

  const type = getType(target)
  let result
  // 如果是可继续遍历类型,初始化该对应类型
  if(deepTag.includes(type)) {
    result = getInit(target, type)
  }

  if(!cache) {
    cache = new Map() //如果没有缓存,则创建
  }

  // 放置循环引用
  if (cache.get(target)) {
    return cache.get(target)
  }
  cache.set(target, result)

  // 拷贝 set
  if (type === setTag) {
    target.forEach(value => {
      result.add(deepClone(value,cache))
    });
    return result
  }

  // 拷贝 map
  if (type === mapTag) {
    target.forEach((value, key) => {
      result.set(key, deepClone(value,cache))
    });
    return result
  }

  // 拷贝对象和数组
  let index = -1
  let keys = isArray ? undefined : Object.keys(target)
  let array = keys || target
  const length = keys ? keys.length : target.length
  while (++index < length) {
    let key = index
    let value = array[index]
    if (keys) {
      key = value
    }
    result[key] = deepClone(target[key], cache)
  }
  return result
}

不可以继续遍历的类型 其他剩余的类型可以统一归类成不可处理的数据类型。 Bool、Number、String、String、Date、Error 这几种类型可以直接用构造函数和原始数据创建一个新对象:

function cloneOtherType(target, type) {
  const Ctor = target.constructor
  switch (type) {
    case boolTag:
    case numberTag:
    case stringTag:
    case errorTag:
    case dateTag:
      return new Ctor(target)
    case regexpTag:
      return cloneReg(target)
    case symbolTag:
      return cloneSymbol(target)
    default:
      return null
  }
}

拷贝正则和 Symbol 类型的方法:

function cloneReg(target) {
  const reFlags = /\w*$/
  const result = new targe.constructor(targe.source, reFlags.exec(targe))
  result.lastIndex = targe.lastIndex
  return result
}

function cloneSymbol(target) {
  return Object(Symbol.prototype.valueOf.call(target))
}

还有其它类型不一一处理,重点是清楚实现深拷贝的整体思路。 改写后的深拷贝为:

function deepClone(target, cache) {
  // 直接返回原始数据类型
  if(!isObject(target)) {
    return target
  }

  const type = getType(target)
  let result
  // 初始化对应类型
  if(deepTag.includes(type)) {
    result = getInit(target, type)
  } else {
    return cloneOtherType(target, type)
  }

  if(!cache) {
    cache = new Map() //如果没有缓存,则创建
  }

  // 放置循环引用
  if (cache.get(target)) {
    return cache.get(target)
  }
  cache.set(target, result)

  // 拷贝 set
  if (type === setTag) {
    target.forEach(value => {
      result.add(deepClone(value,cache))
    });
    return result
  }

  // 拷贝 map
  if (type === mapTag) {
    target.forEach((value, key) => {
      result.set(key, deepClone(value,cache))
    });
    return result
  }

  // 拷贝对象和数组
  let index = -1
  let keys = isArray ? undefined : Object.keys(target)
  let array = keys || target
  const length = keys ? keys.length : target.length
  while (++index < length) {
    let key = index
    let value = array[index]
    if (keys) {
      key = value
    }
    result[key] = deepClone(target[key], cache)
  }
  return result
}

另一种较为简洁的深拷贝写法,适用于常用的部分类型

const deepClone = (target, cache) => {
  if(!cache){
    cache = new Map() // 缓存不能全局,最好临时创建并递归传递
  }
  if(target instanceof Object) {
    if(cache.get(target)) {
      return cache.get(target)
    }
    let result 
    if(target instanceof Function) {
      if(target.prototype) { // 有 prototype 就是普通函数
        result = function(){ return target.apply(this, arguments) }
      } else {
        result = (...args) => { return target.call(undefined, ...args) }
      }
    } else if(target instanceof Array) {
      result = []
    } else if(target instanceof Date) {
      result = new Date(target - 0)
    } else if(target instanceof RegExp) {
      result = new RegExp(target.source, target.flags)
    } else {
      result = {}
    }
    cache.set(target, result)
    for(let key in target) { 
      if(target.hasOwnProperty(key)){
        result[key] = deepClone(target[key], cache) 
      }
    }
    return result
  } else {
    return target
  }
}

笔记参考《现代 JavaScript 教程》,如何写出一个惊艳面试官的深拷贝?阮一峰 ES6 教程JavaScript 内存泄漏教程