【独行长路】深拷贝的实现

199 阅读3分钟

前言

已经有的工具,为什么还需要去手动实现呢?

如果这个不行了的话,就去找下一个替代品,

阅读一下文档直接用不就好了?

前辈们的付出,诚然极大地减轻了我们开发负担,

但我想编程不是一件“能运行”就完了的事。

“为什么这么写就可以达到目的?”

“这个东西到底在其中做了些什么?”

“实现需求暂且按下不表,它会有什么副作用吗?”

“知识”究竟是什么?

一个小白的学习笔记,“独行长路”取自喜欢的角色傻狗刻俄柏。

第一篇,关于深拷贝。参阅自 ConardLi 先生的文章,斗胆做了些修改:

  • 类型上补充了 BigInt,和 Symbol 一样,存在 typeof 1n === 'bigint'typeof Object(1n) === 'object' 这样的两种场景。
  • 使用 Reflect.ownKeys() 来处理键为 Symbol 的场景。
  • 简化了 function 的拷贝。

正文

总体思路如下:

  1. 首先判断值类型与引用类型,值类型的情况下直接返回。
  2. 引用类型下,判断是否可以遍历。不可遍历的情况下,获取其构造函数,并用构造函数创建一个新的返回。
  3. 可遍历的情况下,借助 Map 检查其是否循环引用,若 Map 中已存在,则获取并返回。
  4. 单独处理 MapSet 类型。
  5. 使用 Reflect.ownKeys() 遍历对象的键,递归处理其值。

代码如下:

// 使用 Object.prototype.toString.call(target) 获取的类型
// 可遍历类型
const typeMap = '[object Map]';
const typeSet = '[object Set]';
const typeArray = '[object Array]';
const typeObject = '[object Object]';
const typeArguments = '[object Arguments]';
// 不可遍历类型
const typeBoolean = '[object Boolean]';
const typeString = '[object String]';
const typeNumber = '[object Number]';
const typeBigInt = '[object BigInt]';
const typeDate = '[object Date]';
const typeError = '[object Error]';
const typeSymbol = '[object Symbol]';
const typeRegExp = '[object RegExp]';
const typeFunction = '[object Function]';

// 组装一个可遍历类型的数组,以供下文查询
const iterableTypes = [typeMap, typeSet, typeArray, typeObject, typeArguments];

// 处理 Symbol 的拷贝
// Symbol 不使用 new 关键字,可以使用 Object() 函数来创建一个 Symbol 包装器对象。
// 使用 Symbol.prototype.valueOf.call() 用以获取 Symbol 的值。
const getSymbol = function (target) {
  return Object(Symbol.prototype.valueOf.call(target));
};

// 处理 RegExp 的拷贝,参阅 Lodash 的实现。
// flags 可以用 target.flags 来获取,但是参阅 MDN 该方法的支持很差,所以使用正则的方式获取。
// lastIndex 影响下一次匹配的索引,所以也要拷贝。
const getRegExp = function (target) {
  const regFlags = /\w*$/;
  const newReg = new RegExp(target.source, regFlags.exec(target));
  newReg.lastIndex = target.lastIndex;
  return newReg;
};

// 处理不可遍历类型的拷贝。
// 使用 target.constructor 来获取其构造函数并 new 一个返回。
function getUniterabletypes(target, type) {
  switch (type) {
    case typeBoolean:
    case typeString:
    case typeNumber:
    case typeBigInt:
    case typeDate:
    case typeError:
      return new target.constructor(target);
    case typeSymbol:
      return getSymbol(target);
    case typeRegExp:
      return getRegExp(target);
    default:
      return null;
  }
}

// 判断是否为引用类型的工具函数。
function isReference(target) {
  const type = typeof target;
  return target !== null && (type === 'object' || type === 'function');
}

// 主函数
// 关于 Map 还是 WeakMap,ConardLi 先生的文章评论里有争论,但自己学识不足不解其究竟
// 所以听从先生“如果不懂就不要这么写”,这里使用 Map
function clone(target, map = new Map()) {
  // 非引用类型
  // 这里返回 boolean, number, bigint, string, date, undefined
  if (!isReference(target)) return target;
  // 这里对 function 直接返回,或者参阅 ConardLi 先生的文章
  if (typeof target === 'function') return target;

  // 引用类型
  // 获取其类型
  let type = Object.prototype.toString.call(target);
  
  // 不可继续遍历
  // null, Error, RegExp
  // 下述包装器对象的 typeof === ‘object’,所以在判断非引用类型时会过滤到这里
  // new Boolean, new Number, new String, new Date, Object(Symbol), Object(BigInt)
  if (!iterableTypes.includes(type)) {
    return getUniterabletypes(target, type);
  }
  
  // 可继续遍历
  // map, set, array, object, args
  // 使用其构造函数初始化,会相应的生成 Set, Map 等
  let cloneTarget = new target.constructor();
  // 检查循环引用
  if (map.get(target)) return map.get(target);
  map.set(target, cloneTarget);

  // 处理 Set
  // 注意在 add 时递归的处理其值
  if (type === typeSet) {
    target.forEach((value) => cloneTarget.add(clone(value, map)));
    return cloneTarget;
  }
  // 处理 Map
  // 注意在 set 时递归的处理其值
  if (type === typeMap) {
    target.forEach((value, key) => cloneTarget.set(key, clone(value, map)));
    return cloneTarget;
  }
  // 遍历键,递归的处理其值
  // 考虑到键为 Symbol 的场景,这里使用 Reflect.ownKeys()
  // 参阅阮一峰的 ES6 在线文档:https://es6.ruanyifeng.com/#docs/reflect#Reflect-ownKeys-target
  for (let key of Reflect.ownKeys(target)) {
    cloneTarget[key] = clone(target[key], map);
  }

  return cloneTarget;
}