JS 手写: 深拷贝 (deep clone)

2023-05-17 15:17:13 浏览数 (2)

# JSON.parse()

代码语言:javascript复制
const newObj = JSON.parse(JSON.stringify(obj));

局限性:

  • 无法实现对函数,正则表达式等特殊对象的克隆
  • 会抛弃对象的 constructor,所有的构造函数会指向 Object
  • 对象有循环引用会报错

# 简单手写版

思路:若属性为值类型,直接返回;若属性为引用类型,递归遍历。

代码语言:javascript复制
function deepClone (obj) {
  // 如果值 值类型 或 null ,直接返回
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }

  let copy = {};

  // 如果对象是数组
  if (obj.constructor === Array) {
    copy = [];
  }

  // 遍历对象的每个属性
  for (let k in obj) {
    // 如果 key 是对象的自有属性
    if (obj.hasOwnProperty(k)) {
      // 递归调用 deepClone
      copy[k] = deepClone(obj[k]);
    }
  }

  return copy;
}

# 完整实现

# 简易版及问题

代码语言:javascript复制
JSON.parse(JSON.stringify(obj));

  1. 无法解决 循环引用 的问题
代码语言:javascript复制
const a = { val: 2};
a.target = a;
// 这种情况下 拷贝 会溢出

  1. 无法拷贝一些特殊对象,如 RegExp, Date, Set, Map
  2. 无法拷贝 函数
代码语言:javascript复制
const deepClone = (target) => {
  // 如果是 值类型 或 null ,直接返回
  if (typeof target !== 'object' || target === null) {
    return target;
  }
  const copy = Array.isArray(target) ? [] : {};
  for (let prop in target) {
    if (target.hasOwnProperty(prop)) {
      copy[prop] = deepClone(target[prop]);
    }
  }
  return copy;
}

# 解决循环引用

代码语言:javascript复制
let obj = { val: 2};
obj.target = obj;

deepClone(obj); // 报错: RangeError: Maximum call stack size exceeded

思路:创建一个 Map ,记录已经被拷贝的对象,遇到已经拷贝的对象,直接返回。

代码语言:javascript复制
const isObject = (target) => {
  return (typeof target === 'object' || typeof target === 'function')
    && (target !== null);
};

const deepClone = (target, map = new Map()) => {
  if (map.get(target)) {
    return target;
  }
  if (isObject(target)) {
    map.set(target, true);
    const cloneTarget = Array.isArray(target) ? [] : {};
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {
        cloneTarget[prop] = deepClone(target[prop], map);
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
};

const a = { val: 2 };
a.target = a;
const b = deepClone(a);
console.log(b); // { val: 2, target: {…} }

在 map 上的 key 和 map 构成了强引用,是一种危险操作。 被弱引用的对象可以在任何时候被回收,对于强引用,只要这个强引用还在,那么对象无法被回收。

ES6 提供了 WeakMap,可以解决这个问题。

代码语言:javascript复制
const deepClone = (target, map = new WeakMap()) => {};

# 拷贝特殊对象

# 可继续遍历的对象

思路: 使用 Object.prototype.toString.call(obj) 鉴别可遍历对象

代码语言:javascript复制
const getType = (target) => {
  return Object.prototype.toString.call(target).slice(8, -1);
};

const canTraverse = (target) => {
  const type = getType(target);
  return ['Map', 'Set', 'Array', 'Object', 'Arguments'].includes(type);
};

const deepClone = (target, map = new WeakMap()) => {
  if (!isObject(target)) {
    return target;
  }
  let cloneTarget;
  if (!canTraverse(target)) {
    // TODO 处理不可遍历的对象 
    return;
  } else {
    // 确保对象原型不丢失
    let ctor = target.prototype;
    cloneTarget = new ctor();
  }

  if (map.get(target)) {
    return target;
  }
  map.put(target, true);

  if (getType(target) === 'Map') {
    target.forEach((item, key) => {
      cloneTarget.set(deepClone(key), deepClone(item));
    });
  }

  if (getType(target) === 'Set') {
    target.forEach((item) => {
      cloneTarget.add(deepClone(item));
    });
  }

  // 数组和对象
  for (let prop in target) {
    if (target.hasOwnProperty(prop)) {
      cloneTarget[prop] = deepClone(target[prop]);
    }
  }

  return cloneTarget;
};

# 不可遍历的对象
代码语言:javascript复制
const canNotTraverse = (target) => {
  const type = getType(target);
  return ['Boolean', 'Number', 'String', 'Date', 'Error', 'RegExp', 'Function'].includes(type);
};

const handleRegExp = (target) => {
  return new target.constructor(target.source, target.flags);
};

const handleFunc = (target) => {
  // TODO 处理函数
};

const handleNotTraverse = (target) => {
  const Ctor = target.constructor;
  if (getType(target) === 'RegExp') {
    return handleRegExp(target);
  } else if (getType(target) === 'Function') {
    return handleFunc(target);
  } else {
    return new Ctor(target);
  }
};

# 拷贝函数

在 JS 中有两种函数,一种是普通函数,另一种是箭头函数。每个普通函数都是 Function 的实例,而箭头函数不是任何类的实例,每次调用都是不一样的引用。只需要处理普通函数的情况,箭头函数直接返回它本身就好了。利用原型来区分两者,箭头函数不存在原型。

代码语言:javascript复制
const handleFunc = (func) => {
  if (!func.prototype) {
    return func;
  }

  const bodyReg = /(?<={)(.|n) (?=})/m;
  const paramReg = /(?<=(). (?=)s {)/;
  const funcStr = func.toString();

  const param = paramReg.exec(funcStr)[0];
  const body = bodyReg.exec(funcStr)[0];

  if (!body) {
    return null;
  }
  if (param) {
    const paramArr = param.split(',');
    return new Function(...paramArr, body);
  } else {
    return new Function(body);
  }
};

# 完整实现

代码语言:javascript复制
const getType = obj => Object.prototype.toString.call(obj);

const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;

const canTraverse = {
  '[object Map]': true,
  '[object Set]': true,
  '[object Array]': true,
  '[object Object]': true,
  '[object Arguments]': true,
};
const mapTag = '[object Map]';
const setTag = '[object Set]';
const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

const handleRegExp = (target) => {
  const { source, flags } = target;
  return new target.constructor(source, flags);
}

const handleFunc = (func) => {
  // 箭头函数直接返回自身
  if(!func.prototype) return func;
  const bodyReg = /(?<={)(.|n) (?=})/m;
  const paramReg = /(?<=(). (?=)s {)/;
  const funcString = func.toString();
  // 分别匹配 函数参数 和 函数体
  const param = paramReg.exec(funcString);
  const body = bodyReg.exec(funcString);
  if(!body) return null;
  if (param) {
    const paramArr = param[0].split(',');
    return new Function(...paramArr, body[0]);
  } else {
    return new Function(body[0]);
  }
}

const handleNotTraverse = (target, tag) => {
  const Ctor = target.constructor;
  switch(tag) {
    case boolTag:
      return new Object(Boolean.prototype.valueOf.call(target));
    case numberTag:
      return new Object(Number.prototype.valueOf.call(target));
    case stringTag:
      return new Object(String.prototype.valueOf.call(target));
    case symbolTag:
      return new Object(Symbol.prototype.valueOf.call(target));
    case errorTag: 
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
}

const deepClone = (target, map = new WeakMap()) => {
  if(!isObject(target)) 
    return target;
  let type = getType(target);
  let cloneTarget;
  if(!canTraverse[type]) {
    // 处理不能遍历的对象
    return handleNotTraverse(target, type);
  }else {
    // 这波操作相当关键,可以保证对象的原型不丢失!
    let ctor = target.constructor;
    cloneTarget = new ctor();
  }

  if(map.get(target)) 
    return target;
  map.set(target, true);

  if(type === mapTag) {
    //处理Map
    target.forEach((item, key) => {
      cloneTarget.set(deepClone(key, map), deepClone(item, map));
    })
  }
  
  if(type === setTag) {
    //处理Set
    target.forEach(item => {
      cloneTarget.add(deepClone(item, map));
    })
  }

  // 处理数组和对象
  for (let prop in target) {
    if (target.hasOwnProperty(prop)) {
        cloneTarget[prop] = deepClone(target[prop], map);
    }
  }
  return cloneTarget;
}

0 人点赞