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

写该系列文章的初衷是“让每位前端工程师掌握高频知识点,为工作助力”。这是前端百题斩的第23斩,希望朋友们关注公众号“执鸢者”,用知识武装自己的头脑。

相信老铁们不管是在学习还是面试过程中,都会遇到赋值、浅拷贝、深拷贝,特别是浅拷贝和深拷贝,我记忆比较深刻的遇到这个问题有两次:

  1. 一次系统写出bug就是因为对深浅拷贝理解不清楚;

  2. 百度面试。

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 }
image-20210614163449366.png

如上所示,赋值就是将一个值赋给另一个值,在赋值过程中要注意两点:

  1. 对于基本类型赋值就是在栈内存中开辟一个新的存储区域来存储新的变量;

  2. 对于引用类型赋值,就是将该引用类型的地址,该地址指向堆中的同一值。

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

上述就是简单的浅拷贝过程,可以看到浅拷贝就是将原始对象中的值遍历一层,然后赋值给一个新的对象。在遍历过程中可以获取到一下信息:

  1. 遍历到a属性的时候,其是一个基本类型,所以会在栈内存中创建一个新的存储区域来存储变量。

  2. 遍历到b属性的时候,由于其为引用类型,其会在栈内存中存储器堆地址,从而指向堆内存中的同一对象。

  3. 当通过浅拷贝创建的对象cloneObj中的a属性和b.m属性重新赋值,可以发现a属性值不一样,但b.m属性值却发生了变化,从而验证了上述1、2两条分析。

23.2.2 进阶

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

img
23.2.2.1 基础

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

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

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

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

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

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

23.2.2.2 实现

上面阐述了主要的注意点,下面我们就来实现一下Object.assign(),实现步骤如下所示:

  1. 对目标对象进行判断,不能为null和undefined;

  2. 将目标转换为对象(防止string、number等);

  3. 获取后续源对象自身中的可枚举对象(包含Symbol)复制到目标对象;

  4. 返回该处理好的目标对象;

  5. 利用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 深拷贝

img

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

23.3.1 乞丐版

首先来看一下最简单的深拷贝方式,就是利用JSON.stringify()和JSON.parse(),但是该方式其实是存在很多问题的:

  1. 不能正确处理正则表达式,其会变为空对象;

  2. 不能正确处理函数,其变为undefined;

  3. 不能正常输出值为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);

输出结果如下所示:

image-20210614184005694.png
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);

输出结果如下所示:

image-20210614183858006.png

1.如果觉得这篇文章还不错,来个分享、点赞、在看三连吧,让更多的人也看到~

2.关注公众号执鸢者,领取学习资料,定期为你推送原创深度好文

3.关注公众号进群,里面大佬多多,一起向他们学习

1. 前端百题斩[001]——typeof和instanceof

2. 前端百题斩【002】——js中6种变量声明方式

3. 前端百题斩【003-004】——从基本类型、引用类型到包装对象

4. 前端百题斩【005】—— js中9种遍历对象的方法

5. 前端百题斩【006】——js中三类字符串转数字的方式

6. 前端百题斩【007】——js中必须知道的四种数据类型判断方法

7. 前端百题斩【008-009】——从JavaScript的代码执行过程到函数执行过程

8. 前端百题斩【010】——通俗易懂的JavaScript执行上下文

9. 前端百题斩【011】——通俗易懂的变量对象

10. 前端百题斩【012】——js中作用域及作用域链的真面目

11. 前端百题斩【013】——用“闭包”问题征服面试官

12. 前端百题斩【014】——js中的这些“this”指向都值得了解

13. 前端百题斩【015】——快速手撕call、apply、bind

14. 前端百题斩【016】——原型、构造函数和实例之间的奇妙关系

15. 前端百题斩【017】——一基础、二主线、双机制理解原型链

16. 前端百题斩【018】——从验证点到手撕new操作符

17. 前端百题斩【019】——数组中方法原理早知道

18. 前端百题斩【020】——竟然有五种方式实现flat方法

19. 前端百题斩【021】——通俗易懂的防抖与节流

20. 前端百题斩【022】——开拓思路之三种方式实现字符串转驼峰

21. 2021 年前端宝典【超三百篇】

22. 前端也要懂机器学习(上)

23. 前端也要懂机器学习(下)

24. 学架构助力前端起飞

25. 假如只剩下canvas标签

26. Vue源码思想在工作中的应用

27. 一文搞定Diff算法

28. 百度、小红书三面,均遇“赛马”问题

29. 十五张图带你彻底搞懂从URL到页面展示发生的故事

30. 一文搞懂Cookie、Storage、IndexedDB

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值