JavaScript 自己实现深拷贝 (deep clone)

简介: JavaScript 自己实现深拷贝 (deep clone)

JSON.parse()

const newObj = JSON.parse(JSON.stringify(obj));

局限性:

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

简单手写版

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

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;
}

完整实现

简易版及问题

JSON.parse(JSON.stringify(obj));
  1. 无法解决 循环引用 的问题

    const a = { val: 2};
    a.target = a;
    // 这种情况下 拷贝 会溢出
  2. 无法拷贝一些特殊对象,如 RegExp, Date, Set, Map
  3. 无法拷贝 函数
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;
}

解决循环引用

let obj = { val: 2};
obj.target = obj;

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

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

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: {…} }

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

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

const deepClone = (target, map = new WeakMap()) => {};

拷贝特殊对象

可继续遍历的对象

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

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;
};

不可遍历的对象

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 的实例,而箭头函数不是任何类的实例,每次调用都是不一样的引用。只需要处理普通函数的情况,箭头函数直接返回它本身就好了。利用原型来区分两者,箭头函数不存在原型。

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);
  }
};

完整实现

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;
}
相关文章
|
6月前
|
JSON JavaScript 数据格式
深拷贝和浅拷贝(js的问题)
深拷贝和浅拷贝(js的问题)
31 0
|
1月前
|
JavaScript 前端开发
JavaScript中的深拷贝与浅拷贝
JavaScript中的深拷贝与浅拷贝
49 4
|
2月前
|
JSON JavaScript 数据格式
手写JS实现深拷贝函数
本文介绍了如何实现一个深拷贝函数`deepClone`,该函数可以处理对象和数组的深拷贝,确保拷贝后的对象与原始对象在内存中互不干扰。通过递归处理对象的键值对和数组的元素,实现了深度复制,同时保留了函数类型的值和基础类型的值。
22 3
|
3月前
|
JavaScript 前端开发
JavaScript中的深拷贝与浅拷贝
JavaScript中的深拷贝与浅拷贝
42 2
|
3月前
|
JavaScript 前端开发
JavaScript中的深拷贝和浅拷贝的实现讲解
在JavaScript中,浅拷贝与深拷贝用于复制对象。浅拷贝仅复制基本类型属性,对于引用类型仅复制引用,导致双方共享同一数据,一方修改会影响另一方。深拷贝则完全复制所有层级的数据,包括引用类型,确保双方独立。浅拷贝可通过简单属性赋值实现,而深拷贝需递归复制各层属性以避免共享数据。
72 1
|
3月前
|
JavaScript 前端开发
js中浅拷贝和深拷贝的区别
js中浅拷贝和深拷贝的区别
29 0
|
5月前
|
JSON JavaScript 前端开发
【JavaScript】JavaScript中的深拷贝与浅拷贝详解:基础概念与区别
JavaScript 中,理解数据拷贝的深浅至关重要。浅拷贝(如扩展运算符`...`、`Object.assign()`)仅复制对象第一层,共享内部引用,导致修改时产生意外联动。深拷贝(如自定义递归函数、`_.cloneDeep`或`JSON.parse(JSON.stringify())`)创建独立副本,确保数据隔离。选择哪种取决于性能、数据独立性和资源需求。深拷贝虽慢,但确保安全;浅拷贝快,但需小心引用共享。在面试中,理解这些概念及其应用场景是关键。
129 4
【JavaScript】JavaScript中的深拷贝与浅拷贝详解:基础概念与区别
|
3月前
|
JavaScript 前端开发
js中浅拷贝,深拷贝的实现
js中浅拷贝,深拷贝的实现
34 0
|
4月前
|
JavaScript
js【详解】深拷贝 (含 JSON.parse(JSON.stringify(obj)) 的缺陷,5种手写深拷贝)
js【详解】深拷贝 (含 JSON.parse(JSON.stringify(obj)) 的缺陷,5种手写深拷贝)
163 0
|
4月前
|
存储 JavaScript 前端开发
javascript的栈内存 VS 堆内存(浅拷贝 VS 深拷贝)
javascript的栈内存 VS 堆内存(浅拷贝 VS 深拷贝)
31 0