手撕代码系列(二):深拷贝的多种形态

1,708 阅读5分钟

前言

首先,让我们明确一下深拷贝的目标。

深拷贝不仅将原对象的各个属性逐个复制出去,而且将原对象各个属性所包含的对象也依次采用深拷贝的方法递归拷贝到新对象上。

我们在此基础上补充两个要求

  1. 只拷贝可枚举属性
  2. 不拷贝原型链上原型对象的属性

当然基于不同的业务需要可能目标会有所变化,本文暂不考虑。

深拷贝的实现(最简易版)

首先实现一个最简易的递归深拷贝版本

function deepClone(obj) {
    // 对要拷贝的对象进行一个初始化
    const copyObj = Array.isArray(obj)?[]:{};
    // for in 循环遍历对象的属性
    for (const key in obj){
        // 重点 for in会遍历一个对象除Symbol外的可枚举属性,包括原型链上的
        // 如果只想考虑对象本身的属性,则需要用hasOwnProperty判断
        if (obj.hasOwnProperty(key)) {
            // obj[key]是对象则递归,不是对象则直接拷贝基本类型
            copyObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
        }
    }
    return copyObj;
}
// 测试一下
var obj = {
    a:1,
    b:{}
};
var copy = deepClone(obj)
console.log(copy === obj);// false
copy.a = 2;
console.log(copy.a === obj.a);// false
console.log(copy.b === obj.b);// false

深拷贝的实现(循环引用版)

有时候,对象可能会出现循环引用的情况,那么这时候深拷贝可能会出现无限递归最后栈溢出的情况。因此,对于循环引用的情况我们需要进行一些处理。

// 使用Map存储已经被拷贝过的对象,待拷贝对象地址作为键,拷贝后的对象地址作为值
function deepClone(obj,copyMap = new Map()) {
    // 判断是否当前对象已经拷贝过,若拷贝过直接返回拷贝过的对象
    if(copyMap.has(obj)) return copyMap.get(obj);
    const copyObj = Array.isArray(obj)?[]:{};
    // 添加已经拷贝过的对象
    copyMap.set(obj,copyObj);
    for (const key in obj){
        if (obj.hasOwnProperty(key)) {
            copyObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key],copyMap) : obj[key];
        }
    }
    return copyObj;
}
// 测试一下,obj是一个通过self属性无限引用自身的对象
var obj = {};
obj.self = obj;
var copy = deepClone(obj)
console.log(copy === obj);// false
console.log(copy.self === copy);// true
console.log(copy.self.self === copy);// true

我们在实现的时候使用map去存储已经拷贝过的对象,但是这并不是唯一解,我们也可以自拟数据结构去实现。

深拷贝的实现(Symbol版)

之前有提过hasOwnProperty()不能枚举属性名是Symbol类型的属性,想要拷贝属性名是Symbol的属性,我们需要借助别的函数去实现。

tips:对象属性名在存储时仅能为stringSymbol

让我们聊聊Object.getOwnPropertySymbols()这个方法,它返回一个给定对象自身的所有Symbol 属性的数组(非Symbol属性名不会包含在数组中)

为了减少代码量,我们这里基于简易版进行改造

function deepClone(obj) {
    const copyObj = Array.isArray(obj)?[]:{};
    // 遍历可枚举但非Symbol的属性
    for (const key in obj){
        if (obj.hasOwnProperty(key)) {
            copyObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
        }
    }
    // 遍历Symbol属性
    for (const key of Object.getOwnPropertySymbols(obj)){
        // 严谨一点,我们除去不可枚举的属性
        if (obj.propertyIsEnumerable(key)) {
            copyObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
        }
    }
    return copyObj;
}
// 测试一下
var symbolKeyA = Symbol();//symbolKeyA不可枚举
var symbolKeyB = Symbol();//symbolKeyB可枚举
var obj = Object.create({}, {
  [symbolKeyA]: {
    value: {},
    enumerable: false
  }
});
obj[symbolKeyB] = {};
var copy = deepClone(obj);
console.log(copy[symbolKeyA]); // undefined
console.log(copy[symbolKeyB]); // Object {  }
console.log(copy[symbolKeyB] === obj[symbolKey]);// false

加餐总结

  • 一般的,要遍历由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)用Object.getOwnPropertyNames()

  • 更具体的,如果只要获取可枚举属性(不包括Symbol值作为名称的属性)Object.keys或用for...in循环,但for...in循环还会获取到原型链上的可枚举属性,不过可以使hasOwnProperty()方法过滤掉。

  • 特别的Object.getOwnPropertySymbols() 方法返回一个给定对象自身的所有Symbol 属性的数组(非Symbol属性名不会包含在数组中)。注意,这也包括了不可枚举属性不包括原型链上的属性,可以使用propertyIsEnumerable()方法过滤掉不可枚举属性。

  • 容易被忽略的,静态方法 Reflect.ownKeys() 返回一个由目标对象自身的属性键组成的数组。相当于Object.getOwnPropertyNames()加上Object.getOwnPropertySymbols()

示例代码如下:

  var symbolKeyA = Symbol('可枚举的Symbol');
  var symbolKeyB = Symbol('不可枚举的Symbol');
  var symbolKeyC = Symbol('原型对象上的Symbol');
  var obj = Object.create({}, {
    [symbolKeyA]: {
      value: {},
      enumerable: false
    },
    ['不可枚举的属性']:{
      value: {},
      enumerable: false
    }
  });
  obj[symbolKeyB] = {};
  obj['实例属性'] = {};
  obj.__proto__['原型对象上的属性']={};
  obj.__proto__[symbolKeyC]={};
  
  
  console.log(Reflect.ownKeys(obj));
  //["不可枚举的属性", "实例属性", Symbol(可枚举的Symbol), Symbol(不可枚举的Symbol)]
  
  console.log(Object.getOwnPropertyNames(obj));
  // ["不可枚举的属性", "实例属性"]
  
  console.log(Object.keys(obj));
  // ["实例属性"]
  
  for(const key in obj) {
      console.log(key);
      // "实例属性"
      // "原型对象上的属性"
  }
  
  console.log(Object.getOwnPropertySymbols(obj));
  // [Symbol(可枚举的Symbol), Symbol(不可枚举的Symbol)]
  

注意:上述方法均无法访问到原型对象上的Symbol属性
看晕了?直接上图!

文后作业

请大家想想怎么用 Reflect.ownKeys() 改造上面的例子吧,可以把结果发送在评论区!除此之外Reflect还可以用在Proxy上,这个就等着日后填坑吧。

关于我

喜欢聊天、喜欢分享、喜欢前沿的22届小菜鸡,初来乍到希望得到各位大佬的关注。能有实习/校招机会就更好啦!
个人公众号:鼠子的前端CodeLife