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

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

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


  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 }


640.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)。

640.jpg


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 深拷贝



640.jpg

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


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


输出结果如下所示:


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


输出结果如下所示:


640.png

目录
打赏
0
0
0
0
5
分享
相关文章
前端基础(五)_运算符(算术运算符、赋值运算符、比较运算符、逻辑运算符、三目运算符、运算符优先级和结合性、数据类型的隐式转换)
本文介绍了JavaScript中的算术运算符、赋值运算符、比较运算符、逻辑运算符、三目运算符、运算符优先级和结合性以及数据类型的隐式转换。
108 3
栈在前端中的应用,顺便再了解下深拷贝和浅拷贝!
该文章探讨了栈在前端开发中的应用,并深入讲解了JavaScript中深拷贝与浅拷贝的区别及其实现方法。
栈在前端中的应用,顺便再了解下深拷贝和浅拷贝!
前端深拷贝、浅拷贝,一起手撕深拷贝
【7月更文挑战第2天】JavaScript中的深拷贝和浅拷贝关乎对象复制的独立性。浅拷贝(如`Object.assign()`、扩展运算符)创建新对象,但共享引用类型属性的内存地址,导致修改新对象会影响原始对象。深拷贝(如递归复制)创建完全独立的对象副本,不受原始对象变动影响。`JSON.parse(JSON.stringify(obj))`是简单的深拷贝方法,但无法处理函数、undefined、Symbol及循环引用。手动实现深拷贝需递归遍历并处理循环引用问题,以确保复制的完整性。理解这两者差异对编写健壮的代码至关重要。
48 0
前端 js 经典:深拷贝
前端 js 经典:深拷贝
41 1
web前端之ES6的实用深度解构赋值方法、复杂的解构赋值
web前端之ES6的实用深度解构赋值方法、复杂的解构赋值
92 1
【前端面试题】深拷贝的终极实现
【前端面试题】深拷贝的终极实现
前端学习笔记202306学习笔记第三十八天-Es6-字符串的解构赋值1
前端学习笔记202306学习笔记第三十八天-Es6-字符串的解构赋值1
68 0
前端学习笔记202306学习笔记第三十八天-Es6-字符串的解构赋值1
前端学习笔记202306学习笔记第四十二天-Es6-实现深拷贝2
前端学习笔记202306学习笔记第四十二天-Es6-实现深拷贝2
67 0
前端学习笔记202306学习笔记第四十二天-Es6-实现深拷贝2
前端学习笔记202306学习笔记第三十八天-Es6-数组得结构赋值
前端学习笔记202306学习笔记第三十八天-Es6-数组得结构赋值3
68 0
前端学习笔记202306学习笔记第三十八天-Es6-数组得结构赋值3
前端学习笔记202306学习笔记第三十八天-Es6-数组得结构赋值3
61 0

热门文章

最新文章

  • 1
    【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    13
  • 2
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    24
  • 3
    详解智能编码在前端研发的创新应用
    16
  • 4
    巧用通义灵码,提升前端研发效率
    14
  • 5
    智能编码在前端研发的创新应用
    9
  • 6
    【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
    12
  • 7
    大前端之前端开发接口测试工具postman的使用方法-简单get接口请求测试的使用方法-简单教学一看就会-以实际例子来说明-优雅草卓伊凡
    26
  • 8
    VSCode AI提效工具,通义灵码前端开发体验
    63
  • 9
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    8
  • 10
    以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
    13
  • 1
    【01】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-项目开发实战-优雅草卓伊凡拟开发一个一站式家政服务平台-前期筹备-暂定取名斑马家政软件系统-本项目前端开源-服务端采用优雅草蜻蜓Z系统-搭配ruoyi框架admin后台-全过程实战项目分享-从零开发到上线
    28
  • 2
    VSCode AI提效工具,通义灵码前端开发体验
    63
  • 3
    开箱即用的GO后台管理系统 Kratos Admin - 前端权限
    5
  • 4
    以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
    35
  • 5
    大前端之前端开发接口测试工具postman的使用方法-简单get接口请求测试的使用方法-简单教学一看就会-以实际例子来说明-优雅草卓伊凡
    67
  • 6
    【2025优雅草开源计划进行中01】-针对web前端开发初学者使用-优雅草科技官网-纯静态页面html+css+JavaScript可直接下载使用-开源-首页为优雅草吴银满工程师原创-优雅草卓伊凡发布
    31
  • 7
    巧用通义灵码,提升前端研发效率
    97
  • 8
    【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    149
  • 9
    详解智能编码在前端研发的创新应用
    107
  • 10
    智能编码在前端研发的创新应用
    84
  • AI助理

    你好,我是AI助理

    可以解答问题、推荐解决方案等