4. 手写实现浅拷贝
根据以上对浅拷贝的理解,实现浅拷贝的思路:
- 对基础类型做最基本的拷贝;
- 对引用类型开辟新的存储,并且拷贝一层对象属性。
代码实现:
// 浅拷贝的实现; function shallowCopy(object) { // 只拷贝对象 if (!object || typeof object !== "object") return; // 根据 object 的类型判断是新建一个数组还是对象 let newObject = Array.isArray(object) ? [] : {}; // 遍历 object,并且判断是 object 的属性才拷贝 for (let key in object) { if (object.hasOwnProperty(key)) { newObject[key] = object[key]; } } return newObject; } 复制代码
这里用到了 hasOwnProperty()
方法,该方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性。所有继承了 Object 的对象都会继承到 hasOwnProperty()
方法。这个方法可以用来检测一个对象是否是自身属性。
可以看到,所有的浅拷贝都只能拷贝一层对象。如果存在对象的嵌套,那么浅拷贝就无能为力了。深拷贝就是为了解决这个问题而生的,它能解决多层对象嵌套问题,彻底实现拷贝。
二、深拷贝的原理与实现
深拷贝是指,对于简单数据类型直接拷贝他的值,对于引用数据类型,在堆内存中开辟一块内存用于存放复制的对象,并把原有的对象类型数据拷贝过来,这两个对象相互独立,属于两个不同的内存地址,修改其中一个,另一个不会发生改变。
1. JSON.stringify()
JSON.parse(JSON.stringify(obj))
是比较常用的深拷贝方法之一,它的原理就是利用JSON.stringify
将JavaScript
对象序列化成为JSON字符串),并将对象里面的内容转换成字符串,再使用JSON.parse
来反序列化,将字符串生成一个新的JavaScript对象。
这个方法是目前我在公司项目开发中使用最多的深拷贝的方法,也是最简单的方法。
使用示例:
let obj1 = { a: 0, b: { c: 0 } }; let obj2 = JSON.parse(JSON.stringify(obj1)); obj1.a = 1; obj1.b.c = 1; console.log(obj1); // {a: 1, b: {c: 1}} console.log(obj2); // {a: 0, b: {c: 0}} 复制代码
这个方法虽然简单粗暴,但也存在一些问题,在使用该方法时需要注意:
- 拷贝的对象中如果有函数,undefined,symbol,当使用过
JSON.stringify()
进行处理之后,都会消失。 - 无法拷贝不可枚举的属性;
- 无法拷贝对象的原型链;
- 拷贝 Date 引用类型会变成字符串;
- 拷贝 RegExp 引用类型会变成空对象;
- 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;
- 无法拷贝对象的循环应用,即对象成环 (
obj[key] = obj
)。
在日常开发中,上述几种情况一般很少出现,所以这种方法基本可以满足日常的开发需求。如果需要拷贝的对象中存在上述情况,还是要考虑使用下面的几种方法。
2. 函数库lodash
该函数库也有提供_.cloneDeep
用来做深拷贝,可以直接引入并使用:
var _ = require('lodash'); var obj1 = { a: 1, b: { f: { g: 1 } }, c: [1, 2, 3] }; var obj2 = _.cloneDeep(obj1); console.log(obj1.b.f === obj2.b.f);// false 复制代码
这里附上lodash中深拷贝的源代码供大家学习:
/** * value:需要拷贝的对象 * bitmask:位掩码,其中 1 是深拷贝,2 拷贝原型链上的属性,4 是拷贝 Symbols 属性 * customizer:定制的 clone 函数 * key:传入 value 值的 key * object:传入 value 值的父对象 * stack:Stack 栈,用来处理循环引用 */ function baseClone(value, bitmask, customizer, key, object, stack) { let result // 标志位 const isDeep = bitmask & CLONE_DEEP_FLAG // 深拷贝,true const isFlat = bitmask & CLONE_FLAT_FLAG // 拷贝原型链,false const isFull = bitmask & CLONE_SYMBOLS_FLAG // 拷贝 Symbol,true // 自定义 clone 函数 if (customizer) { result = object ? customizer(value, key, object, stack) : customizer(value) } if (result !== undefined) { return result } // 非对象 if (!isObject(value)) { return value } const isArr = Array.isArray(value) const tag = getTag(value) if (isArr) { // 数组 result = initCloneArray(value) if (!isDeep) { return copyArray(value, result) } } else { // 对象 const isFunc = typeof value == 'function' if (isBuffer(value)) { return cloneBuffer(value, isDeep) } if (tag == objectTag || tag == argsTag || (isFunc && !object)) { result = (isFlat || isFunc) ? {} : initCloneObject(value) if (!isDeep) { return isFlat ? copySymbolsIn(value, copyObject(value, keysIn(value), result)) : copySymbols(value, Object.assign(result, value)) } } else { if (isFunc || !cloneableTags[tag]) { return object ? value : {} } result = initCloneByTag(value, tag, isDeep) } } // 循环引用 stack || (stack = new Stack) const stacked = stack.get(value) if (stacked) { return stacked } stack.set(value, result) // Map if (tag == mapTag) { value.forEach((subValue, key) => { result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack)) }) return result } // Set if (tag == setTag) { value.forEach((subValue) => { result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack)) }) return result } // TypedArray if (isTypedArray(value)) { return result } // Symbol & 原型链 const keysFunc = isFull ? (isFlat ? getAllKeysIn : getAllKeys) : (isFlat ? keysIn : keys) const props = isArr ? undefined : keysFunc(value) // 遍历赋值 arrayEach(props || value, (subValue, key) => { if (props) { key = subValue subValue = value[key] } assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack)) }) // 返回结果 return result } 复制代码
3. 手写实现深拷贝
(1)基础递归实现
实现深拷贝的思路就是,使用for in来遍历传入参数的属性值,如果值是基本类型就直接复制,如果是引用类型就进行递归调用该函数,实现代码如下:
function deepClone(source) { //判断source是不是对象 if (source instanceof Object == false) return source; //根据source类型初始化结果变量 let target = Array.isArray(source) ? [] : {}; for (let i in source) { // 判断是否是自身属性 if (source.hasOwnProperty(i)) { //判断数据i的类型 if (typeof source[i] === 'object') { target[i] = deepClone(source[i]); } else { target[i] = source[i]; } } } return target; } console.log(clone({b: {c: {d: 1}}})); // {b: {c: {d: 1}}}) 复制代码
这样虽然实现了深拷贝,但也存在一些问题:
- 不能复制不可枚举属性以及 Symbol 类型;
- 只能对普通引用类型的值做递归复制,对于 Date、RegExp、Function 等引用类型不能正确拷贝;
- 可能存在循环引用问题。
(2)优化递归实现
上面只是实现了一个基础版的深拷贝,对于上面存在的几个问题,可以尝试去解决一下:
- 使用
Reflect.ownKeys()
方法来解决不能复制不可枚举属性以及 Symbol 类型的问题。Reflect.ownKeys()
方法会返回一个由目标对象自身的属性键组成的数组。它的返回值等同于:Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
; - 当参数值为 Date、RegExp 类型时,直接生成一个新的实例并返回;
- 利用
Object.getOwnPropertyDescriptors()
方以获得对象的所有属性以及对应的特性。简单来说,这个方法返回给定对象的所有属性的信息,包括有关getter和setter的信息。它允许创建对象的副本并在复制所有属性(包括getter和setter)时克隆它。 - 使用
Object.create()
方法创建一个新对象,并继承传入原对象的原型链。Object.create()
方法会创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
。 - 使用 WeakMap 类型作为 Hash 表,WeakMap 是弱引用类型,可以防止内存泄漏,所以可以用来检测循环引用,如果存在循环,则引用直接返回 WeakMap 存储的值。WeakMap的特性就是,保存在其中的对象不会影响垃圾回收,如果WeakMap保存的节点,在其他地方都没有被引用了,那么即使它还在WeakMap中也会被垃圾回收回收掉了。在深拷贝的过程当中,里面所有的引用对象都是被引用的,为了解决循环引用的问题,在深拷贝的过程中,希望有个数据结构能够记录每个引用对象有没有被使用过,但是深拷贝结束之后这个数据能自动被垃圾回收,避免内存泄漏。
代码实现:
function deepClone (obj, hash = new WeakMap()) { // 日期对象直接返回一个新的日期对象 if (obj instanceof Date){ return new Date(obj); } //正则对象直接返回一个新的正则对象 if (obj instanceof RegExp){ return new RegExp(obj); } //如果循环引用,就用 weakMap 来解决 if (hash.has(obj)){ return hash.get(obj); } // 获取对象所有自身属性的描述 let allDesc = Object.getOwnPropertyDescriptors(obj); // 遍历传入参数所有键的特性 let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc) hash.set(obj, cloneObj) for (let key of Reflect.ownKeys(obj)) { if(typeof obj[key] === 'object' && obj[key] !== null){ cloneObj[key] = deepClone(obj[key], hash); } else { cloneObj[key] = obj[key]; } } return cloneObj } 复制代码
可以使用以下数据进行测试:
let obj = { num: 1, str: 'str', boolean: true, und: undefined, nul: null, obj: { name: '对象', id: 1 }, arr: [0, 1, 2], func: function () { console.log('函数') }, date: new Date(1), reg: new RegExp('/正则/ig'), [Symbol('1')]: 1, }; Object.defineProperty(obj, 'innumerable', { enumerable: false, value: '不可枚举属性' }); obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj)) obj.loop = obj // 将loop设置成循环引用的属性 let cloneObj = deepClone(obj) console.log('obj', obj) console.log('cloneObj', cloneObj) 复制代码
运行结果如下: