信老铁们不管是在学习还是面试过程中,都会遇到赋值、浅拷贝、深拷贝,特别是浅拷贝和深拷贝,我记忆比较深刻的遇到这个问题有两次:
- 一次系统写出bug就是因为对深浅拷贝理解不清楚;
- 百度面试。
23.1 赋值
赋值指的就是将一个变量直接赋值给另一个变量,如下所示:
const a1 = 10; const a2 = a1; console.log(a2); // 10 const b1 = { m: 10, n: 20 }; const b2 = b1; console.log(b2); // { m: 10, n: 20 }
如上所示,赋值就是将一个值赋给另一个值,在赋值过程中要注意两点:
- 对于基本类型赋值就是在栈内存中开辟一个新的存储区域来存储新的变量;
- 对于引用类型赋值,就是将该引用类型的地址,该地址指向堆中的同一值。
23.2 浅拷贝
23.2.1 基本实现
浅拷贝指的就是循环遍历对象一遍,将该对象上的属性赋值到另一个对象上。在这个过程中属性值为基本类型则拷贝的就是基本类型的值;若该值为引用类型,则拷贝的就是就是一个内存地址。
function clone(source) { if (!(typeof source === 'object' && source !== null)) { return source; } const target = {}; // 只考虑Object类型 for (let [key, value] of Object.entries(source)) { target[key] = value; } return target; } const obj = { a: 10, b: { m: 20 } }; const cloneObj = clone(obj); cloneObj.a = 20; cloneObj.b.m = 30; console.log(obj); // { a: 10, b: { m: 30 } } console.log(cloneObj); // { a: 20, b: { m: 30 } }
上述就是简单的浅拷贝过程,可以看到浅拷贝就是将原始对象中的值遍历一层,然后赋值给一个新的对象。在遍历过程中可以获取到一下信息:
- 遍历到a属性的时候,其是一个基本类型,所以会在栈内存中创建一个新的存储区域来存储变量。
- 遍历到b属性的时候,由于其为引用类型,其会在栈内存中存储器堆地址,从而指向堆内存中的同一对象。
- 当通过浅拷贝创建的对象cloneObj中的a属性和b.m属性重新赋值,可以发现a属性值不一样,但b.m属性值却发生了变化,从而验证了上述1、2两条分析。
23.2.2 进阶
既然本章我们讲了浅拷贝,那么不得不了解
Object.assign()
,该方法就是一个浅拷贝的过程,用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
23.2.2.1 基础
要实现一个函数首先应该了解一个函数,对于该方法的基本使用就不再赘述,下面主要讲几个注意点:
- 如果目标对象与源对象有同名属性(或多个源对象有同名属性),则后面的属性会覆盖前面的属性;
- 如果只有一个参数,Object.assign会直接返回该参数。如果该参数不是对象,则会先转为对象,然后再返回;(注意:由于undefined和null无法转为对象,将它们作为参数会报错)
- 非对象参数出现在源对象位置,这些参数会转化为对象,如果无法转成对象便跳过(所以undefined和null不会报错)。(注意:字符串会以数组形式复制到目标对象,其它不会)
- 只复制源对象的自身属性(不复制继承属性),也不复制不可枚举的属性;
- 属性名为Symbol值的属性也会被Object.assign复制。
23.2.2.2 实现
上面阐述了主要的注意点,下面我们就来实现一下Object.assign(),实现步骤如下所示:
- 对目标对象进行判断,不能为null和undefined;
- 将目标转换为对象(防止string、number等);
- 获取后续源对象自身中的可枚举对象(包含Symbol)复制到目标对象;
- 返回该处理好的目标对象;
- 利用Object.defineProperty()将该函数配置为不可枚举的挂载到Object上。
function ObjectAssign(target, ...sources) { // 对第一个参数进行判断,不能为undefined和null if (target === undefined || target === null) { throw new TypeError('cannot convert first argument to object'); } // 将第一个参数转换为对象 const targetObj = Object(target); // 将源对象(source)自身的所有可枚举属性复制到目标对象(target) for (let i = 0; i < sources.length; i++) { let source = sources[i]; // 对于undefined和null在源对象中不会报错,会直接跳过 if (source !== undefined && source !== null) { // 将源角色转换成对象 // 需要将源角色自身的可枚举属性(包含Symbol值的属性)进行复制 // Reflect.ownKeys(obj) 返回一个数组,包含对象自身的所有属性,不管属性名是Symbol还是字符串,也不管是否可枚举 const keysArrays = Reflect.ownKeys(Object(source)); for (let nextIndex = 0; nextIndex < keysArrays.length; nextIndex++) { const nextKey = keysArrays[nextIndex]; // 去除不可枚举属性 const desc = Object.getOwnPropertyDescriptor(source, nextKey); if (desc !== undefined && desc.enumerable) { targetObj[nextKey] = source[nextKey]; } } } } return targetObj; } // 由于挂载到Object的assign是不可枚举的,直接挂载上去是可枚举的,所以采用这种方式 if (typeof Object.myAssign !== 'function') { Object.defineProperty(Object, "myAssign", { value: ObjectAssign, writable: true, enumerable: false, configurable: true }); } const target = { a: 10 }; const source1 = { b: 20, c: 30 }; const source2 = { c: 40 }; console.log(Object.assign(target, source1, source2)); // { a: 10, b: 20, c: 40 } console.log(Object.myAssign(target, source1, source2)); // { a: 10, b: 20, c: 40 }
23.3 深拷贝
深拷贝其实就是浅拷贝的进阶版,因为浅拷贝只循环遍历了一层数据,对于引用类型拷贝的是对象的地址,但是深拷贝会进行多层的遍历,将所有数据进行数据层面的拷贝。下面就利用三种方式实现深拷贝。(这篇文章写得很好,大家可以一起看一下)
23.3.1 乞丐版
首先来看一下最简单的深拷贝方式,就是利用JSON.stringify()和JSON.parse(),但是该方式其实是存在很多问题的:
- 不能正确处理正则表达式,其会变为空对象;
- 不能正确处理函数,其变为undefined;
- 不能正常输出值为undefined的内容。
function cloneDeep(source) { return JSON.parse(JSON.stringify(source)); } const obj = { a: 10, b: undefined, c: /\w/g, d: function() { return true; } }; console.log(obj); // { a: 10, b: undefined, c: /\w/g, d: [Function: d] } console.log(cloneDeep(obj)); // { a: 10, c: {} }
23.3.2 递归版
既然乞丐版有这么多问题,那么就尝试一下“浅拷贝+递归”的方式实现一下。
function cloneDeep(source) { // 如果输入的为基本类型,直接返回 if (!(typeof source === 'object' && source !== null)) { return source; } // 判断输入的为数组还是对象,进行对应的创建 const target = Array.isArray(source) ? [] : {}; for (let [key, value] of Object.entries(source)) { // 此处应该去除一些内置对象,根据需要可以自己去除,本初只去除了RegExp对象 if (typeof value === 'object' && value !== null && !(value instanceof RegExp)) { target[key] = cloneDeep(value); } else { target[key] = value; } } return target; } const obj = { a: 10, b: undefined, c: /\w/g, d: function() { return true; }, e: { m: 20, n: 30 } }; const result = cloneDeep(obj); result.e.m = 100; console.log('拷贝前:', obj); console.log('拷贝后:', result);
输出结果如下所示:
23.3.3 循环方式
利用递归的方式实现深拷贝,其实是存在爆栈的风险的,下面就将递归的方式改为循环的方式。
// 循环方式 function cloneDeep(source) { if (!(typeof source === 'object' && source !== null)) { return source; } const root = Array.isArray(source) ? [] : {}; // 定义一个栈 const loopList = [{ parent: root, key: undefined, data: source, }]; while (loopList.length > 0) { // 深度优先 const node = loopList.pop(); const parent = node.parent; const key = node.key; const data = node.data; // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素 let res = parent; if (typeof key !== 'undefined') { res = parent[key] = Array.isArray(data) ? [] : {}; } for (let [childKey, value] of Object.entries(data)) { if (typeof value === 'object' && value !== null && !(value instanceof RegExp)) { loopList.push({ parent: res, key: childKey, data: value }); } else { res[childKey] = value; } } } return root; } const obj = { a: 10, b: undefined, c: /\w/g, d: function() { return true; }, e: { m: 20, n: 30 } }; const result = cloneDeep(obj); result.e.m = 100; console.log('拷贝前:', obj); console.log('拷贝后:', result);
输出结果如下所示: