小和尚学习 - JS shallow clone 和 deep clone

480 阅读4分钟

在这里插入图片描述

决定一个人的一生,以及整个命运的,只是一瞬之间。——歌德

今天,小和尚和大伙聊聊一个面试家常菜:请手写一个 JS 深克隆。

在正式开始前,先了解下克隆

英文“clone”的音译,在台湾与港澳一般意译为复制或转殖,是利用生物技术由无性生殖产生与原个体完全相同基因组织后代的过程。 ——百度百科

简单来说就是单独拷贝一份与原数据一模一样的新数据

JS 中的克隆有以下两种:

浅克隆

将目标对象的的第一层属性拷贝到一个新的对象中,其中对原始类型属性,进行值的拷贝;如果是引用类型属性,则是进行引用地址的拷贝。

  • 数组的浅克隆

    • array.concat
    • array.slice
    • [...array]
  • 对象的浅克隆

    • Object.assign
    • { ...object }
  • 缺点

    • 新对象的引用类型属性修改时,会影响到原对象
    • 符号属性(例:[Symbol])不能拷贝
    • 不可枚举属性不能拷贝
// 测试用例
var obj = {
	bigInt: BigInt(12312),
	set:new Set([2]),
	map:new Map([['a',22],['b',33]]),
	num: 0,
	str: '',
	boolean: true,
	unf: undefined,
	nul: null,
	obj: {
		name: '我是一个对象',
		id: 1
	},
	arr: [0, 1, 2],
	func: function () {
			console.log('我是一个函数')
	},
	date: new Date(0),
	reg: new RegExp('/我是一个正则/ig'),
	[Symbol('1')]: 1,
};

Object.defineProperty(obj, 'innumerable', {
	enumerable: false,
	value: '不可枚举属性'
});
obj.loop = obj;

// ------------------------------- 测试代码 -------------------------------
var cloneObj = {};

for (var prop in obj) {
   cloneObj[prop] = obj[prop];
}

// 修改了新对象,原对象受到了影响
cloneObj.arr[0] = 2;
obj.arr[0]; // 2

深克隆

拷贝目标对象属性到新对象上,对于引用类型属性,是将引用地址指向的堆内存空间里的内容,重新生成一份新的,并拷贝到新对象上。

下面介绍一些常见的深克隆

丐中丐版

使用 JSON.parse(JSON.stringify(obj)) 实现

缺点:

  1. 不能拷贝 MapSetRegExpFunction 类型的属性和符号属性
  2. 不能拷贝不可枚举属性
  3. Date 类型的属性值,拷贝后会显示为字符串类型
  4. 不能处理循环引用属性
  5. 反序列化后,新对象的原型会丢失,新对象 的 __proto__ 自动指向了 Object.prototype
// 测试用例
var obj = {
	set:new Set([2]),
	map:new Map([['a',22],['b',33]]),
	num: 0,
	str: '',
	boolean: true,
	unf: undefined,
	nul: null,
	obj: {
		name: '我是一个对象',
		id: 1
	},
	arr: [0, 1, 2],
	func: function () {
			console.log('我是一个函数')
	},
	date: new Date(0),
	reg: new RegExp('/我是一个正则/ig'),
	[Symbol('1')]: 1,
};

Object.defineProperty(obj, 'innumerable', {
	enumerable: false,
	value: '不可枚举属性'
});
// obj.loop 属性的值为自身,构成了循环引用属性
// obj.loop = obj;

// ------------------------------- 测试代码 -------------------------------
var cloneObj = JSON.parse(JSON.stringify(obj));
cloneObj.arr[0] = 2;
obj.arr[0]; // 0

大众版

递归遍历原对象的所有属性,将属性一个一个拷贝到新对象

优点:

  1. 增加了对 Function 类型属性的拷贝

缺点:

  1. 不能拷贝 MapSetRegExpDate 类型的属性和符号属性
  2. 不能拷贝不可枚举属性
  3. 不能处理循环引用属性(会引起爆栈)
  4. 容易出现栈溢出情况
// 测试代码
var obj = {
	bigInt: BigInt(12312),
	set:new Set([2]),
	map:new Map([['a',22],['b',33]]),
	num: 0,
	str: '',
	boolean: true,
	unf: undefined,
	nul: null,
	obj: {
		name: '我是一个对象',
		id: 1
	},
	arr: [0, 1, 2],
	func: function () {
			console.log('我是一个函数')
	},
	date: new Date(0),
	reg: new RegExp('/我是一个正则/ig'),
	[Symbol('1')]: 1,
};

Object.defineProperty(obj, 'innumerable', {
	enumerable: false,
	value: '不可枚举属性'
});
// obj.loop = obj;

// ------------------------------- 测试代码 -------------------------------
// 深克隆函数
function deepClone(obj) {
	var cloneObj;
	
	if (obj && typeof obj === 'object') { // object || array
		cloneObj = Array.isArray(obj) ? [] : {};
		
		for (var prop in obj) {
			cloneObj[prop] = deepClone(obj[prop]);
		}
	} else { // primitive
		return obj;
	}
}

var cloneObj = deepClone(obj);
cloneObj.arr[0] = 2;
obj.arr[0]; // 0

升级版

这个版本就是在大众版的基础上,增加了对其他类型属性的拷贝处理,这里由于代码太多就直接贴图了

在这里插入图片描述

终极版

优点:

  1. 解决了递归栈溢出问题
  2. 能拷贝目前所有 js 类型属性
  3. 解决了循环引用属性问题
  4. 拷贝原型对象属性
// 测试用例
let obj = {
    bigInt: BigInt(12312),
    set: new Set([2]),
    map: new Map([['a', 22], ['b', 33]]),
    num: 0,
    str: '',
    boolean: true,
    unf: undefined,
    nul: null,
    obj: {
        name: '我是一个对象',
        id: 1,
    },
    arr: [0, 1, 2],
    fn() {
        return '我是一函数'
    },
    date: new Date(0),
    reg: new RegExp('/我是一个正则/ig'),
    [Symbol('1')]: 1,
};

Object.defineProperty(obj, 'innumerable', {
    enumerable: false,
    value: '不可枚举属性',
    writable: false,
});
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj));
obj.loop = obj;

// ------------------------------- 测试代码 -------------------------------
// 判断是否是引用数据类型
deepClone.isComplexDataType = obj => obj !== null && (typeof obj === 'function' || typeof obj === 'object');
// 存放 typeof === "object" 的构造器,用于区别 obj 是否是普通对象
deepClone.objType = [Date, RegExp, WeakMap, WeakSet, Map, Set];

function deepClone(obj, hash = new WeakMap()) {
    /** tip:能递归进入该函数的都是引用类型数据 **/

    // 判断是否有缓存,如果有则直接返回,解决了递归爆栈的情况
    // 例:obj.loop = obj:当这样形成环后,如果递归进入 deepClone,会返回第一次创建的 cloneObj
    if (hash.has(obj)) return hash.get(obj);
    // 如果不是普通对象,则拷贝一个新 obj 返回
    if (deepClone.objType.includes(obj.constructor)) return new obj.constructor(obj);

    // 获取目标对象的所有属性描述对象
    const allDescriptions = Object.getOwnPropertyDescriptors(obj);
    // 原型拷贝
    const cloneObj = Object.create(Object.getPrototypeOf(obj), allDescriptions);

    // 缓存拷贝的对象
    hash.set(obj, cloneObj);

	// Reflect.ownKeys 以数组形式返回对象的属性名(包括符号属性和不可枚举属性)
    for (let key of Reflect.ownKeys(obj)) {
        const value = obj[key];

        // 原始类型属性直接返回
        // 引用类型属性继续递归deepClone
        cloneObj[key] = (deepClone.isComplexDataType(value) && typeof value !== 'function') ?
            deepClone(value, hash) : value;
    }

    return cloneObj;
}

const cloneObj = deepClone(obj);
cloneObj.arr[0] = 2;
obj.arr[0]; // 0