前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JS 手写: 深拷贝 (deep clone)

JS 手写: 深拷贝 (deep clone)

作者头像
Cellinlab
发布2023-05-17 15:17:13
5810
发布2023-05-17 15:17:13
举报
文章被收录于专栏:Cellinlab's BlogCellinlab's Blog

# 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;
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021/1/7,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • # JSON.parse()
  • # 简单手写版
  • # 完整实现
    • # 简易版及问题
      • # 解决循环引用
        • # 拷贝特殊对象
          • # 可继续遍历的对象
          • # 不可遍历的对象
        • # 拷贝函数
          • # 完整实现
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档