# JavaScript深拷贝与浅拷贝机制详解

# 1. 基础知识准备

# 1.1 数据类型判断

基本数据类型的特点:直接存储在栈(stack)中的数据

引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里

首先我们要确定哪些值是我们需要拷贝的内容,比如基础数据类型number null undefined boolean string 而引用数据类型是Object,具体的又包含function object date regexp array等(error arguments等暂不考虑 symbol作为新增数据类型单独介绍)

// 数据类型判断函数
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
const getType = (target) => {
  return Object.prototype.toString.call(target);
};
const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 1.2 遍历属性操作

const parent = Object.create(Object.prototype, {
  a: {
    value: 123,
    writable: true,
    enumerable: true,
    configurable: true,
  },
});
// parent继承自Object.prototype,有一个可枚举的属性a(enumerable:true)。

const child = Object.create(parent, {
  b: {
    value: 2,
    writable: true,
    enumerable: true,
    configurable: true,
  },
  c: {
    value: 3,
    writable: true,
    enumerable: false,
    configurable: true,
  },
});
// child 继承自 parent ,b可枚举,c不可枚举
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

# 1.2.1 for in

for in 会遍历自身及原型链上可枚举属性,如果只需要获取对象的实例属性,可以使用hasOwnProperty()进行过滤

// eslint-disable-next-line guard-for-in
for (const key in child) {
  console.log(key);
}
// b a
1
2
3
4
5

使用for..in遍历数组它自动过滤掉了不存在的元素,对于存在的元素且值为undefined或者'null'仍然会有效输出

const colors = ['red', 'green', 'blue'];
// 将数组长度变为10
colors.length = 10;
// 再添加一个元素的数组末尾
colors.push('yellow');

// eslint-disable-next-line guard-for-in
for (const i in colors) {
  console.log(i); // 0 1 2 10
}
1
2
3
4
5
6
7
8
9
10

# 1.2.2 Object.keys

Object.keys 会将对象自身的可枚举属性的key输出(相当for...in使用hasOwnProperty)

console.log(Object.keys(child));
// ["b"]
1
2

# 1.2.3 Object.getOwnPropertyNames

Object.getOwnPropertyNames 会将对象自身所有的属性的key输出(包括不可枚举属性但不包括Symbol值作为名称的属性)

console.log(Object.getOwnPropertyNames(child));
// ["b","c"]
1
2

# 1.2.4 遍历方法汇总

方法 作用
for in 对象自身及原型链上可枚举属性
Object.keys 对象自身的可枚举属性
Object.getOwnPropertyNames 对象自身所有的属性(包括不可枚举的属性)
Object.hasOwnProperty 判断某个对象是否含有指定的属性(不包含原型链上的继承属性)
Object.propertyIsEnumerable 判断指定的属性名是否可枚举

# 2. 浅拷贝介绍

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

const a1 = {
  b: {
    c: {},
  },
};

const a2 = shallowClone(a1); // 浅拷贝
a2.b.c === a1.b.c; // true

const a3 = deepClone(a1); // 深拷贝
a3.b.c === a1.b.c; // false
1
2
3
4
5
6
7
8
9
10
11

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。

# 2.1 Object.assign

Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象。但是 Object.assign()进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。

  • Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。
  • 如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性
if (typeof Object.assign !== 'function') {
  // 定义assign方法
  Object.defineProperty(Object, 'assign', {
    value: function assign(target, varArgs) { // .length of function is 2
      'use strict';
      if (target === null) { // 第一个参数为空,则抛错
        throw new TypeError('Cannot convert undefined or null to object');
      }
      const to = Object(target);
      // 遍历剩余所有参数

      for (let index = 1; index < arguments.length; index++) {
        // eslint-disable-next-line prefer-rest-params
        const nextSource = arguments[index];
        // 参数为空,则跳过,继续下一个
        if (nextSource !== null) {
          for (const nextKey in nextSource) {
            // 如果不为空且可枚举,则直接浅拷贝赋值
            if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
              to[nextKey] = nextSource[nextKey];
            }
          }
        }
      }
      return to;
    },
    writable: true, // 是否可以改变
    configurable: true, // 属性是否配置,以及可否删除
  });
}
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

# 2.2 Array.prototype.concat/slice

Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组

原数组的元素会按照下述规则拷贝:

  • 对象引用(而不是实际对象):concat将对象引用复制到新数组中。 原始数组和新数组都引用相同的对象。 也就是说,如果引用的对象被修改,则更改对于新数组和原始数组都是可见的。 这包括也是数组的数组参数的元素。
  • 数据类型如字符串,数字和布尔(不是StringNumberBoolean对象):concat将字符串和数字的值复制到新数组中。
const a = [1, 2, {
  name: 'liam',
}];
const b = a.concat();
b[2].name = 'tom';
console.log(a[2].name); // tom
1
2
3
4
5
6

# 2.3 展开运算符...

展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同

# 3. 深拷贝

function createData (deep, breadth) {
  const data = {};
  let temp = data;
  for (let i = 0; i < deep; i++) {
    // eslint-disable-next-line no-multi-assign
    temp = temp.data = {};
    for (let j = 0; j < breadth; j++) {
      temp[j] = j;
    }
  }
  return data;
}
console.log(createData(3)); // 3层深度,每层有0个数据 {data: {data: {data: {}}}}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 3.1 一行代码的深拷贝

# 3.1.1 实现

function cloneDeep (source) {
  return JSON.parse(JSON.stringify(source));
}
1
2
3

# 3.1.2 数据溢出

通过模拟数据测试溢出问题

cloneDeep(createData(10000)); // Maximum call stack size exceeded
1

看起来 cloneDeep 内部也是使用递归的方式

# 3.1.3 循环引用

const a = {};
a.a = a;

cloneDeep(a); // Uncaught TypeError: Converting circular structure to JSON
1
2
3
4

JSON.stringify内部做了循环引用的检测

# 3.1.4 其他问题

JSON.parse / JSON.stringify深拷贝其他问题

  • date对象成了字符串
  • 函数/undefined直接丢失
  • 正则/Error成了一个空对象 {}
  • NaN、Infinity和-Infinity,则序列化的结果会变成 null
  • 只能序列化对象的可枚举的自有属性,如果有对象是由构造函数生成的,则序列化的结果会丢弃对象的 constructor

# 3.2 基础版

function cloneDeep1(target) {
  if (typeof target === 'object' && target !== null) {
    const cloneTarget = {};
    // eslint-disable-next-line guard-for-in
    for (const key in target) {
      cloneTarget[key] = cloneDeep1(target[key]);
    }
    return cloneTarget;
  }
  return target;
}
1
2
3
4
5
6
7
8
9
10
11

# 3.3 数组类型版

function cloneDeep2(target) {
  if (typeof target === 'object' && target !== null) {
    const cloneTarget = Array.isArray(target) ? [] : {};
    // eslint-disable-next-line guard-for-in
    for (const key in target) {
      cloneTarget[key] = cloneDeep2(target[key]);
    }
    return cloneTarget;
  }
  return target;
}
1
2
3
4
5
6
7
8
9
10
11

# 3.4 循环引用版

const a = {};
a.a = a;
cloneDeep2(a); // Uncaught RangeError: Maximum call stack size exceeded
// 因为递归进入死循环导致栈内存溢出了
1
2
3
4

我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构

  • 检查map中有无克隆过的对象
  • 有 - 直接返回
  • 没有 - 将当前对象作为key,克隆对象作为value进行存储
  • 继续克隆
function cloneDeep3(target, map = new Map()) {
  if (typeof target === 'object' && target !== null) {
    const cloneTarget = Array.isArray(target) ? [] : {};
    if (map.get(target)) {
      return map.get(target);
    }
    map.set(target, cloneTarget);
    // eslint-disable-next-line guard-for-in
    for (const key in target) {
      cloneTarget[key] = cloneDeep3(target[key], map);
    }
    return cloneTarget;
  }
  return target;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 3.5 递归爆栈

cloneDeep3(createData(10000,1));报错:Uncaught RangeError: Maximum call stack size exceeded

将递归改为循环

function cloneDeep4(target) {
  const root = {};
  const map = new Map();
  const loopList = [{ parent: root, isRoot: undefined, data: target }];
  while (loopList.length) {
    const node = loopList.pop();
    const { parent, key, data } = node;
    let result = {};
    if (typeof key === 'undefined') { // 是根节点
      result = parent;
    } else {
      if (map.get(data)) { // 判断是否是循环引用
        parent[key] = map.get(data);
        continue;
      }
      parent[key] = Array.isArray(data) ? [] : {};
      result = parent[key]; // 修改引用指向
      map.set(data, parent[key]);
    }
    for (const i in data) {
      if (typeof data[i] === 'object' && data[i] !== null) {
        // 下一次循环
        loopList.push({
          parent: result,
          key: i,
          data: data[i],
        });
      } else {
        result[i] = data[i];
      }
    }
  }
  return root;
}
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

# 3.6 不同类型拷贝

# 3.6.1 函数

function cloneFunction(func) {
  const bodyReg = /(?<={)(.|\n)+(?=})/m;
  const paramReg = /(?<=\().+(?=\)\s+{)/;
  const funcString = func.toString();
  if (func.prototype) {
    const param = paramReg.exec(funcString);
    const body = bodyReg.exec(funcString);
    if (body) {
      if (param) {
        const paramArr = param[0].split(',');
        // eslint-disable-next-line no-new-func
        return new Function(...paramArr, body[0]);
      }
      // eslint-disable-next-line no-new-func
      return new Function(body[0]);
    }
    return null;
  }
  // eslint-disable-next-line no-eval
  return eval(funcString);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 3.6.2 正则

function cloneReg(targe) {
  const reFlags = /\w*$/;
  const result = new targe.constructor(targe.source, reFlags.exec(targe));
  result.lastIndex = targe.lastIndex;
  return result;
}
1
2
3
4
5
6

# 3.6.3 Symbol

function cloneSymbol(targe) {
  return Object(Symbol.prototype.valueOf.call(targe));
}
1
2
3

# 3.6.4 其他类型

function cloneOtherType(targe, type) {
  const Ctor = targe.constructor;
  switch (type) {
  case boolTag:
  case numberTag:
  case stringTag:
  case errorTag:
  case dateTag:
    return new Ctor(targe);
  case regexpTag:
    return cloneReg(targe);
  case symbolTag:
    return cloneSymbol(targe);
  case funcTag:
    return cloneFunction(targe);
  default:
    return null;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 3.7 优化

# 3.7.1 WeakMap

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的

我们默认创建一个对象:const obj = {},就默认创建了一个强引用的对象,我们只有手动将obj = null,它才会被垃圾回收机制进行回收,如果是弱引用对象,垃圾回收机制会自动帮我们回收。

举个例子:如果我们使用Map的话,那么对象间是存在强引用关系

let obj = { name: 'liam' };
const target = new Map();
target.set(obj, 'test');
obj = null;
1
2
3
4

虽然我们手动将obj,进行释放,然是target依然对obj存在强引用关系,所以这部分内存依然无法被释放

如果是WeakMap的话,target和obj存在的就是弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉

# 3.7.2 便利对象属性方式

遍历数组和对象都使用了for in这种方式,实际上for in在遍历时效率是非常低的

while的效率是最好的,所以,我们可以想办法把for in遍历改变为while遍历

# 3.8 完整代码

// 数据类型判断函数
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
const getType = (target) => {
  return Object.prototype.toString.call(target);
};
const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];
const isObject = (target) => {
  const type = typeof target;
  return target !== null && (type === 'object' || type === 'function');
};
const cloneFunction = (func) => {
  const bodyReg = /(?<={)(.|\n)+(?=})/m;
  const paramReg = /(?<=\().+(?=\)\s+{)/;
  const funcString = func.toString();
  if (func.prototype) {
    const param = paramReg.exec(funcString);
    const body = bodyReg.exec(funcString);
    if (body) {
      if (param) {
        const paramArr = param[0].split(',');
        // eslint-disable-next-line no-new-func
        return new Function(...paramArr, body[0]);
      }
      // eslint-disable-next-line no-new-func
      return new Function(body[0]);
    }
    return null;
  }
  // eslint-disable-next-line no-eval
  return eval(funcString);
};
const cloneReg = (targe) => {
  const reFlags = /\w*$/;
  const result = new targe.constructor(targe.source, reFlags.exec(targe));
  result.lastIndex = targe.lastIndex;
  return result;
};
const cloneSymbol = (targe) => {
  return Object(Symbol.prototype.valueOf.call(targe));
};

const cloneOtherType = (targe) => {
  const type = getType(targe);
  const Ctor = targe.constructor;
  switch (type) {
  case boolTag:
  case numberTag:
  case stringTag:
  case errorTag:
  case dateTag:
    return new Ctor(targe);
  case regexpTag:
    return cloneReg(targe);
  case symbolTag:
    return cloneSymbol(targe);
  case funcTag:
    return cloneFunction(targe);
  default:
    return null;
  }
};
const getInit = (target) => {
  const Ctor = target.constructor;
  return new Ctor();
};

const getCloneData = (target, list, map) => {
  // 避免循环引用
  if (map.get(target)) {
    return map.get(target);
  }
  if (deepTag.includes(getType(target))) {
    // 子元素是深拷贝类型
    const initData = getInit(target);
    list.push({ parent: initData, data: target });
    map.set(target, initData);
    return initData;
  }
  if (!isObject(target)) {
    return target;
  }
  return cloneOtherType(target);
};

const cloneDeep = (target) => {
  // 克隆原始类型
  if (!isObject(target)) {
    return target;
  }
  // 初始化
  const type = getType(target);
  let cloneTarget;
  if (deepTag.includes(type)) {
    // 需要深拷贝的类型
    cloneTarget = getInit(target, type);
  } else {
    return cloneOtherType(target);
  }
  const map = new WeakMap();
  const loopList = [{ parent: cloneTarget, data: target }];

  while (loopList.length) {
    const node = loopList.pop();
    const { parent, data } = node;

    const type = getType(data);
    // 克隆set
    if (type === setTag) {
      data.forEach((value) => {
        parent.add(getCloneData(value, loopList, map));
      });
    }
    // 克隆map
    if (type === mapTag) {
      data.forEach((value, key) => {
        parent.set(key, getCloneData(value, loopList, map));
      });
    }
    // 克隆数组、 对象、 argument
    Object.keys(data).forEach((key) => {
      parent[key] = getCloneData(data[key], loopList, map);
    });
  }
  return cloneTarget;
};
// 测试用例
const a = {
  a: new Set([1, 'abc', new Date(), new Set([2, 'abc'])]),
  b: new Map([
    [1, [1, 2, 3]],
    [2, 'two'],
    [3, new Date()],
  ]),
};
const b = cloneDeep(a);
console.log(a, b);
// 循环引用测试用例
const c = {};
c.c = c;
const d = cloneDeep(c);
console.log(c, d);
// 递归爆栈问题
const createData = (deep, breadth) => {
  const data = {};
  let temp = data;
  for (let i = 0; i < deep; i++) {
    // eslint-disable-next-line no-multi-assign
    temp = temp.data = {};
    for (let j = 0; j < breadth; j++) {
      temp[j] = j;
    }
  }
  return data;
};
const e = createData(10000, 2);
const f = cloneDeep(e);
console.log(e, f);
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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170

# 4. 参考文档

关于 JSON.parse(JSON.stringify(obj)) 实现深拷贝的一些坑 (opens new window)

深拷贝的终极探索(90%的人都不知道) (opens new window)

如何写出一个惊艳面试官的深拷贝? (opens new window)

【进阶4-3期】面试题之如何实现一个深拷贝 (opens new window)

ConardLi.github.io (opens new window)

Last Updated: 8/29/2025, 5:36:30 PM