JavaScript的深浅拷贝

2021/6/17 JavaScript

# 深浅拷贝的区别

假设B复制了A,当修改B时,看A是否会发生变化,如果A也跟着变了,说明这是浅拷贝,如果A没变,那就是深拷贝。

JavaScript中深拷贝只针对引用类型的值(如arrayobject),而基本类型的值不存在深浅拷贝一说(如numberstringboolean),因此这里只考虑引用类型的数据:objectarray

# 浅拷贝的实现

# 直接赋值

const arr1 = [1, 2, 3];
const arr2 = arr1;
console.log(arr2); // [1, 2, 3]
arr2[1] = 22;
console.log(arr1); // [1, 22, 3]
console.log(arr2); // [1, 22, 3]
1
2
3
4
5
6

# 深拷贝的实现

# 只有基本类型的数组

  • 创建新数组,循环往里添加。
  • slice: const arr2 = arr1.slice(0);
  • concat: const arr2 = arr1.concat();
  • Array.from(): const arr2 = Array.from(arr1);
  • 扩展运算符: const arr2 = [...arr1];

# 只有基本类型的对象

  • 创建新对象,循环往里添加。
  • Object.assign(): const obj2 = Object.assign({}, obj1)
  • 扩展运算符: const obj2 = { ...obj1 };

# 数组或对象里面还有引用数据

  1. JSON.parse(JSON.stringify())

    const obj1 = { name: 'obj1', child: { name: 'child', } };
    const obj2 = JSON.parse(JSON.stringify(obj1));
    
    1
    2

    WARNING

    JSON.parse(JSON.stringify())实现深拷贝对象(object)有以下弊端:

    • 时间对象Date,格式化之后变成字符串。
    • RegExpError对象,格式化后变成空对象{}
    • 函数、undefinedSymbol格式化后键值对将丢失。
    • NaNInfinity-Infinity,格式化后变成null
    • JSON.stringify()只能序列化对象的可枚举的自有属性,如果对象中的属性值是由构造函数生成的,格式化会丢弃对象的constructor
    • 如果对象中存在循环引用的情况也无法正确实现深拷贝。
  2. 递归

    /* eslint-disable no-restricted-syntax */
    /* eslint-disable no-use-before-define */
    /**
     * @param {*} value
     * @returns {string}
     */
    function getType(value) {
      // "[object Object]"
      return Object.prototype.toString.call(value).slice(8, -1).toLocaleLowerCase();
    }
    
    /**
     * @param {Array} value
     * @param {Map} cache
     * @returns {Array}
     */
    function cloneArray(value, cache) {
      const result = [];
      cache.set(value, result);
      value.forEach((el) => {
        result.push(deepClone(el, cache));
      });
      return result;
    }
    
    /**
     * @param {RegExp} value
     * @param {Map} cache
     * @returns {RegExp}
     */
    function cloneRegexp(value, cache) {
      const result = new RegExp(value.source, value.global);
      cache.set(value, result);
      return result;
    }
    
    /**
     * @param {Object} value
     * @param {Map} cache
     * @returns {Object}
     */
    function cloneObject(value, cache) {
      const result = {};
      cache.set(value, result);
      const keys = Object.getOwnPropertyNames(value).concat(Object.getOwnPropertySymbols(value));
      keys.forEach((key) => {
        result[key] = deepClone(value[key], cache);
      });
      return result;
    }
    
    /**
     * @param {Function} value
     * @param {Map} cache
     * @returns {Function}
     */
    function cloneFunction(value, cache) {
      // eslint-disable-next-line no-new-func
      const result = new Function(`return ${value.toString()}`)();
      cache.set(value, result);
      return result;
    }
    
    /**
     * @param {Date} value
     * @param {Map} cache
     * @returns {Date}
     */
    function cloneDate(value, cache) {
      const result = new Date(value);
      cache.set(value, result);
      return result;
    }
    
    /**
     * @param {Map} value
     * @param {Map} cache
     * @returns {Map}
     */
    function cloneMap(value, cache) {
      const result = new Map();
      cache.set(value, result);
      for (const [key, val] of value.entries()) {
        result.set(key, deepClone(val), cache);
      }
      return result;
    }
    
    /**
     * @param {Set} value
     * @param {Map} cache
     * @returns {Set}
     */
    function cloneSet(value, cache) {
      const result = new Set();
      cache.set(value, result);
      for (const val of value) {
        result.add(deepClone(val), cache);
      }
      return result;
    }
    
    /**
     * @param {*} value
     * @param {Map} cache
     * @returns {*}
     */
    function cloneNormal(value, cache) {
      cache.set(value, value);
      return value;
    }
    
    /**
     * @description 深拷贝
     * @author fengchunqi
     * @date 2021-06-17 14:35:13
     * @param {*} data
     * @param {Map} cache
     * @returns {*}
     */
    function deepClone(data, cache = new Map()) {
      if (cache.has(data)) {
        return data;
      }
      const type = getType(data);
      switch (type) {
        case 'array':
          return cloneArray(data, cache);
        case 'regexp':
          return cloneRegexp(data, cache);
        case 'object':
          return cloneObject(data, cache);
        case 'function':
          return cloneFunction(data, cache);
        case 'date':
          return cloneDate(data, cache);
        case 'map':
          return cloneMap(data, cache);
        case 'set':
          return cloneSet(data, cache);
        default:
          // number、string、boolean、undefined、null、ymbol、bigint
          // 其他 promise htmlcollection nodelist...
          return cloneNormal(data, cache);
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    // 测试
    const obj = {
      Number: 1,
      String: 'string',
      Boolean: true,
      Undefined: undefined,
      Null: null,
      Function: function aaa() {
        console.log('aaa');
      },
      Date: new Date(2021, 5, 17),
      RegExp: /a-z/gi,
      Array: [1, 2, 3, 4, 5],
      Object: {
        name: 'object',
        value: 111,
      },
    };
    const copy = deepClone(obj);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
  3. MessageChannel

    function deepClone(value) {
      return new Promise((resolve, reject) => {
        const { port1, port2 } = new MessageChannel();
        port1.postMessage(value);
        port2.onmessage = (msg) => {
          resolve(msg.data);
        };
      });
    }
    
    deepClone(obj).then((val) => {
      console.log(val);
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    注意:经测试发现MessageChannel的方式支持循环引用,但是不支持拷贝函数(可能是考虑到函数拷贝(new Function)的安全问题)。

最近更新: 2024年09月27日 17:26:57