前端百题斩—赋值、浅拷贝、深拷贝大PK

2023-05-27 0 799

前端百题斩—赋值、浅拷贝、深拷贝大PK

写该系列产品该文的本意是“让每人后端技师掌控低频习题,为组织工作助推”。

坚信老铁们无论是在自学却是复试操作过程中,单厢碰到表达式、浅复本、深复本,不光是浅复本和深复本,我梦境较为真切的碰到那个难题有三次:

一场控制系统写下bug是即使对厚薄复本认知不确切;

腾讯复试。

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 }
前端百题斩—赋值、浅拷贝、深拷贝大PK

如上右图,表达式是将两个值赋给另两个值,在表达式操作过程中要注意两点:

对于基本类型表达式是在栈内存中开辟两个新的存储区域来存储新的表达式;

对于引用类型表达式,是将该引用类型的地址,该地址指向堆中的同一值。

2 浅复本

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两条分析。

2.2 进阶

既然本章我们讲了浅复本,那么不得不了解Object.assign(),该方法是两个浅复本的操作过程,用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。

前端百题斩—赋值、浅拷贝、深拷贝大PK

2.2.1 基础

要实现两个函数首先应该了解两个函数,对于该方法的基本使用就不再赘述,下面主要讲几个注意点:

如果目标对象与源对象有同名属性(或多个源对象有同名属性),则后面的属性会覆盖前面的属性;

如果只有两个参数,Object.assign会间接返回该参数。如果该参数不是对象,则会先转为对象,然后再返回;(注意:由于undefined和null无法转为对象,将它们作为参数会报错)

非对象参数出现在源对象位置,这些参数会转化为对象,如果无法转成对象便跳过(所以undefined和null不会报错)。(注意:字符串会以数组形式复制到目标对象,其它不会)

只复制源对象的自身属性(不复制继承属性),也不复制不可枚举的属性;

属性名为Symbol值的属性也会被Object.assign复制。

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 }

3 深复本

前端百题斩—赋值、浅拷贝、深拷贝大PK

深复本其实是浅复本的进阶版,即使浅复本只循环遍历了一层数据,对于引用类型复本的是对象的地址,但是深复本会进行多层的遍历,将所有数据进行数据层面的复本。下面就利用三种方式实现深复本。(这篇该文写得很好,大家可以一起看一下)

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: {} }

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);

输出结果如下表所示右图:

前端百题斩—赋值、浅拷贝、深拷贝大PK

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);

输出结果如下表所示右图:

前端百题斩—赋值、浅拷贝、深拷贝大PK
举报/反馈

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务