您的位置:

JS深拷贝详解

一、概述

在JS开发中,我们常常需要复制或者克隆一个对象。对于简单数据类型,如数字、字符串、布尔值等,这是很容易实现的。但是对于复杂数据类型,如对象或者数组,就需要用到JS的深拷贝。深拷贝是指将一个对象完全复制一份,并开辟一个新的内存空间存放新对象,新对象的改动不会影响原对象。相比之下,JS的浅拷贝只是复制对象的引用,并没有开辟新的内存空间,因此改动新对象也会影响原对象。

二、基本实现方法

JS的深拷贝可以通过两种方法实现。

1. JSON.parse()

function deepClone(obj) {
  return JSON.parse(JSON.stringify(obj));
}

通过JSON.stringify()将对象转化为字符串,然后再通过JSON.parse()将字符串转化为新的对象,新对象就是原对象的深拷贝。这种方法的优点是实现简单,代码量小,但是缺点也显而易见:无法处理对象中含有函数、正则表达式等非JSON格式数据类型。而且该方法在处理大型对象的时候性能很低,因为需要进行两次序列化和反序列化操作。

2. 递归

function deepClone(obj, hash = new WeakMap()) {
  if (Object(obj) !== obj) return obj;
  if (hash.has(obj)) return hash.get(obj);
  let clonedObj = new obj.constructor();
  hash.set(obj, clonedObj);
  if (obj instanceof Map) {
    obj.forEach((value, key) => {
      clonedObj.set(deepClone(key, hash), deepClone(value, hash));
    });
  } else if (obj instanceof Set) {
    obj.forEach(value => {
      clonedObj.add(deepClone(value, hash));
    });
  } else {
    Object.getOwnPropertyNames(obj).forEach(prop => {
      if (Object.getOwnPropertyDescriptor(obj, prop).enumerable) {
        clonedObj[prop] = deepClone(obj[prop], hash);
      }
    });
  }
  return clonedObj;
}

递归方法则不会存在以上的问题,可以处理复杂对象,但是要注意对象中是否包含自引用的情况,因此需要引入哈希表记忆化处理。

三、对象中含有函数的深拷贝

使用递归方式进行深拷贝的时候,如果对象中含有函数,就会出现深拷贝失误的情况。因为递归过程中不会拷贝函数。因此需要单独处理对象中的函数,这可以分为两步:

1. 拷贝函数

let _toString = Object.prototype.toString;
let _hasOwnProperty = Object.prototype.hasOwnProperty;

function isFunction(obj) {
  return _toString.call(obj) === '[object Function]' || typeof obj === 'function';
}

function cloneFunction(func) {
  let bodyReg = /(?<={)(.|\n)+(?=})/m;
  let paramReg = /(?<=\().+(?=\)\s+{)/;
  let funcString = func.toString();
  if (func.prototype) {
    let params = paramReg.exec(funcString);
    let body = bodyReg.exec(funcString);

    if (body) {
      if (params) {
        params = params[0].split(',').map(param => param.trim());
        return new Function(...params, body[0]);
      } else {
        return new Function(body[0]);
      }
    } else {
      return null;
    }
  } else {
    return eval(funcString);
  }
}

function copy(obj, hash = new WeakMap()) {
  if (Object(obj) !== obj) return obj;
  if (hash.has(obj)) return hash.get(obj);
  let type = _toString.call(obj);
  let copiedObj;
  switch (type) {
    case '[object Array]':
      copiedObj = new obj.constructor();
      break;
    case '[object Function]':
      return cloneFunction(obj);
    case '[object RegExp]':
      copiedObj = new RegExp(obj.source, obj.flags);
      break;
    case '[object Date]':
      copiedObj = new Date(obj.getTime());
      break;
    case '[object Set]':
      copiedObj = new Set();
      break;
    case '[object Map]':
      copiedObj = new Map();
      break;
    default:
      copiedObj = new obj.constructor();
  }
  hash.set(obj, copiedObj);
  if (obj instanceof Map) {
    obj.forEach((value, key) => {
      copiedObj.set(copy(key, hash), copy(value, hash));
    });
  } else if (obj instanceof Set) {
    obj.forEach(value => {
      copiedObj.add(copy(value, hash));
    });
  } else {
    for (let prop in obj) {
      if (_hasOwnProperty.call(obj, prop)) {
        if (type === '[object Array]' || type === '[object Object]') {
          copiedObj[prop] = copy(obj[prop], hash);
        } else {
          Object.defineProperty(copiedObj, prop, Object.getOwnPropertyDescriptor(obj, prop));
        }
      }
    }   
  }
  return copiedObj;
}

上述代码实现了函数的拷贝,其中cloneFunction()函数就是用于拷贝函数的。

2. 拷贝对象中的函数

function deepClone(obj, hash = new WeakMap()) {
  if (Object(obj) !== obj) return obj;
  if (hash.has(obj)) return hash.get(obj);
  let clonedObj = copy(obj, hash);
  hash.set(obj, clonedObj);
  return clonedObj; 
}

在进行递归遍历后,调用copy()方法对对象进行拷贝,并记录到哈希表中。这样即可实现深拷贝。

四、应用场景

JS的深拷贝主要用于解决对象的克隆和数据格式转化等问题。在以下场景中深拷贝会被广泛应用:

1. React组件之间的通信

在React组件内部,有时需要将一个组件的状态传递给子组件,这时候就需要使用深拷贝将状态复制一份,以防止子组件改变父组件的状态,从而避免数据污染的情况。

2. 前端缓存

在前端开发中,经常需要缓存一些数据到本地存储中。如果数据源是一个对象,使用深拷贝可以保证数据不会在缓存的过程中被修改,从而避免数据完整性的问题。

3. 数据结构的复制

在JS中,集合类数据结构(例如Map,Set等)是常用的存储数据的方式。在使用时,需要对这些数据结构进行复制,以便进行修改、删除等操作。使用深拷贝可以确保复制后的数据和原数据完全独立,防止由于互相引用导致结果不可预知。

4. 数据类型转化

在开发中,有时需要将一个数据格式转换成另一个格式。例如将一个JSON对象转换为XML或CSV格式,可以通过先将JSON对象深拷贝得到一个JS对象,然后再将JS对象转换为需要的格式。

五、总结

JS的深拷贝对于复制对象是一项非常关键的技术,掌握深拷贝的方法有助于提高代码效率和数据完整性。本文介绍了两种实现深拷贝的方法,以及如何处理对象中的函数,同时对深拷贝的应用场景作了介绍。