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 遍历属性操作
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.1 for in
for in 会遍历自身及原型链上的可枚举属性,如果只需要获取对象的实例属性,可以使用hasOwnProperty()进行过滤
// eslint-disable-next-line guard-for-in
for (const key in child) {
console.log(key);
}
// b a
使用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.2 Object.keys
Object.keys 会将对象自身的可枚举属性的key输出(相当for...in使用hasOwnProperty)
console.log(Object.keys(child));
// ["b"]
1.2.3 Object.getOwnPropertyNames
Object.getOwnPropertyNames 会将对象自身所有的属性的key输出(包括不可枚举属性但不包括Symbol值作为名称的属性)
console.log(Object.getOwnPropertyNames(child));
// ["b","c"]
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
浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。
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, // 属性是否配置,以及可否删除
});
}
2.2 Array.prototype.concat/slice
Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组
原数组的元素会按照下述规则拷贝:
- 对象引用(而不是实际对象):
concat将对象引用复制到新数组中。 原始数组和新数组都引用相同的对象。 也就是说,如果引用的对象被修改,则更改对于新数组和原始数组都是可见的。 这包括也是数组的数组参数的元素。 - 数据类型如字符串,数字和布尔(不是
String,Number、Boolean对象):concat将字符串和数字的值复制到新数组中。
const a = [1, 2, {
name: 'liam',
}];
const b = a.concat();
b[2].name = 'tom';
console.log(a[2].name); // tom
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: {}}}}
3.1 一行代码的深拷贝
3.1.1 实现
function cloneDeep (source) {
return JSON.parse(JSON.stringify(source));
}
3.1.2 数据溢出
通过模拟数据测试溢出问题
cloneDeep(createData(10000)); // Maximum call stack size exceeded
看起来 cloneDeep 内部也是使用递归的方式
3.1.3 循环引用
const a = {};
a.a = a;
cloneDeep(a); // Uncaught TypeError: Converting circular structure to JSON
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;
}
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;
}
3.4 循环引用版
const a = {};
a.a = a;
cloneDeep2(a); // Uncaught RangeError: Maximum call stack size exceeded
// 因为递归进入死循环导致栈内存溢出了
我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。
这个存储空间,需要可以存储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;
}
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;
}
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);
}
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;
}
3.6.3 Symbol
function cloneSymbol(targe) {
return Object(Symbol.prototype.valueOf.call(targe));
}
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;
}
}
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;
虽然我们手动将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);