当前位置: 首页 > news >正文

js中深浅拷贝的实现方式(含图解原理)

关于赋值,浅拷贝,深拷贝

提前熟知:

  • 栈内存(stack):会自动分配的内存空间,它由系统自动释放
  • 堆内存(heap):动态分配的内存及大小,不一定会自动释放
  • 基本数据类型:String, Number, Boolean, undefined, null, Symbol
  • 引用数据类型:Object, Array, Function
  • 在JS中,数据类型分为基本数据类型和引用数据类型两种,对于基本数据类型来说,它的值直接存储在栈内存中,而对于引用类型来说,它在栈内存中仅仅存储了一个引用,而真正的数据存储在堆内存中

赋值:赋值是将某一数值或对象赋给某个变量的过程

        var a = 1;
        var b = a;
        console.log(b)  // 1

        a = 2;
        console.log(b)  // 1

        b = 3;
        console.log(a) // 2
  • 基本数据类型:我们发现,对于基本数据类型的数据而言,当进行赋值时,系统会自动对变量b在栈内存中开辟一块新的内存进行存储,变量a与变量b的值互不影响,相互独立
  • 我们可以画个图辅助理解:
    在这里插入图片描述
        let a2 = { a:1, b:'Trist' };
        let b2 = a2;
        console.log(b2) // {a: 1, b: "Trist"}

        b2.a = 2;
        console.log(a2.a) // 2
  • 引用数据类型: 通过对于引用数据类型的实验,我们发现与基本数据类型不同的是,两个变量之间会相互干扰。为什么会出现这种现象呢?其实是因为引用数据类型存储在堆内存中,当进行赋值时,系统为变量b2传入了变量a2指向堆内存中地址的指针,实际上他们俩指向的是同一个对象,所以当第二个变量的值改变的时候,第一个变量的值也会改变。
  • 同样的我们可以画个图辅助理解:
    在这里插入图片描述

注:深浅拷贝只针对于引用数据类型

浅拷贝

        function clone(obj) {
            var cloneObj = {};
            for(var key of Object.keys(obj)) {
                cloneObj[key] = obj[key];
            }
            return cloneObj;
        }
        
        var a3 = {
            a: 1,
            b: 'Trist',
            c: { d: 1 }
        }
        var b3 = clone(a3)
        console.log(b3)  // {a: 1,b: "Trist",c: {d: 1}}

        a3.a = 2;
        console.log(b3.a) // 1

        b3.b = '张三',
        console.log(a3.b) // Trist

        b3.c.d = 2;
        console.log(a3.c.d) // 2
  • 实现原理:创建一个拷贝的方法 clone(),通过遍历传入对象的键名及键值,然后赋值给一个空对象,这样就完成了对原对象的浅拷贝。
  • 实验发现:当我们拿到a3的浅拷贝后,我们发现,对于对象中的基本数据类型而言,他们之间是互不影响的,而对于引用数据而言,他们会相互干扰。
  • 理解浅拷贝:浅拷贝只复制了原对象中最外层的属性,也就是拷贝了其基本类型的数据,而对于引用类型数据而言,它仅复制了其引用,指向的地址还是原对象的地址。
  • 图解:
    在这里插入图片描述

注:通过对浅拷贝的认识,如果我们要实现对更深层级的数据不仅是单纯的引用,则需要对原对象内所有的属性值进行遍历递归,这就是我们的深拷贝了

深拷贝

        function deepClone(obj, cloneObj) {
            var cloneObj = cloneObj || {};
            for(var i in obj) {
                // 通过遍历判断属性是否为引用类型,此处注意null因为历史遗留bug通过typeof输出为object
                if(typeof obj[i] === 'object' && typeof obj[i] !== null) {
                    // 判断引用值是否为数据 obj[i] instanceof Array
                    cloneObj[i] = (obj[i].constructor === Array) ? [] : {};
                    // 进行递归
                    deepClone(obj[i], cloneObj[i]);
                }else {
                    cloneObj[i] = obj[i];
                }
            }
            return cloneObj;
        }

        var b4 = deepClone(a4,b4);
        console.log(b4) 

        a4.a = 2;
        console.log(b4.a) // 1

        b4.b = '张三',
        console.log(a4.b) // Trist

        console.log(a4.c) // 1
        console.log(b4.c) // 1
        b4.c.d = 2;
        console.log(a4.c) // 1
        console.log(b4.c) // 2

        console.log(a4.size) // [1, 2, 3]
        console.log(b5.size) // [1, 2, 3]
        b5.size.push(4,5,6)
        console.log(a4.size) // [1, 2, 3]
        console.log(b5.size) // [1, 2, 3, 4, 5, 6]
  • 实现原理:首先进行遍历,判断原对象内的属性是否还有对象以及空值null,若有则判断对象是否为数组,有则赋值空数组,无则赋值空对象,然后进行递归。
  • 实验发现:不管是原对象内的基本数据类型,还是对象,或者数组,我们在进行属性的修改添加修改时,发现他们都各自独立,互不影响。
  • 理解深拷贝:深拷贝不会拷贝引用类型的引用,而是将引用类型的值全部拷贝一份,形成一个新的引用类型,这样就不会发生引用错乱的问题,使得我们可以多次使用同样的数据,而不用担心数据之间会起冲突。
  • 图解:
    在这里插入图片描述

浅拷贝实现方式

原生JS(上文案例)

ES6 assign方法

Object.assign() 拷贝的是属性值,只实现了对第一层的深拷贝,而不能进行更深层的深拷贝,所以归根结底属于浅拷贝

		var assign1 = Object.assign({}, a4);
        console.log(assign1); // {a: 2, b: "Trist", c: {d: 1}, size: (3) [1, 2, 3]}

ES5 属性描述符

了解即可

        function simpleClone(obj) {
            var copy = Object.create(Object.getPrototypeOf(obj));
            Object.getOwnPropertyNames(obj).forEach(key => {
                var desc = Object.getOwnPropertyDescriptor(obj, key);
                Object.defineProperty(copy, key, desc);
            })
            return copy;
        }
        var es5 = simpleClone(a3);
        console.log(es5) // {a: 2, b: "Trist", c: {d: 1}}

深拷贝实现方式

原生JS (上文案例)

下面这种是封装比较好的原生深拷贝写法:

  • 用new obj.constructor ()构造函数新建一个空的对象,而不是使用{}或者[],这样可以保持原形链的继承;
  • 用obj.hasOwnProperty(key)来判断属性是否来自原型链上,因为for…in…也会遍历其原型链上的可枚举属性。
  • 上面的函数用到递归算法,在函数有名字,而且名字以后也不会变的情况下,这样定义没有问题。但问题是这个函数的执行与函数名 factorial 紧紧耦合在了一起。为了消除这种紧密耦合的现象,需要使用 arguments.callee。
        function deepClone3(obj) {
            if(obj === null ) return null;
            if(typeof obj !== 'object') return obj;
            if(obj.constructor === Date) return new Date(obj);
            var newObj = new obj.constructor();
            for(var key in obj) {
                if(obj.hasOwnProperty(key)) {
                    var val = obj[key];
                    newObj[key] = typeof val === 'object' ? arguments.callee(val) : val;
                }
            }
            return newObj;
        }
        var bbb = deepClone3(a4);
        console.log(bbb); // {a: 2, b: "Trist", c: {d: 1}, size: (3) [1, 2, 3]}

JSON

仅能处理Number, String, Boolean, Array 这种扁平对象,也即是能被JSON直接表示的数据结构类型

        function JSONClone(obj) {
            return JSON.parse(JSON.stringify(obj))
        }
        var json = JSONClone(a4);
        console.log(json) // {a: 2, b: "Trist", c: {d: 1}, size: (3) [1, 2, 3]}

jQuery

注意引入第三方库

 		<script src="jquery-3.3.1.min.js"></script>
 		var jquery1 = $.extend(true, {}, a4);
        console.log(jquery1); // {a: 2, b: "Trist", c: {d: 1}, size: (3) [1, 2, 3]}

Lodash

注意引入第三方库

		<script src="lodash.min.js"></script>
    	var lodash1 = _.cloneDeep(a4);
        console.log(lodash1); // {a: 2, b: "Trist", c: {d: 1}, size: (3) [1, 2, 3]}

相关文章:

  • Java刷题知识点之File对象常用功能:获取文件名称、获取文件路径、获取文件大小、获取文件修改时间、创建与删除、判断、重命名、查看系统根目录、容量获取、获取某个目录下内容、过滤器...
  • 查看httpd状态
  • js中如何判断引用值为数组(几种不同方式的详解)
  • 项目代码重用
  • js中数组去重的几种实现方式(区别)
  • java创建文件和目录
  • JS For循环中嵌套setTimeout()方法的理解
  • ubuntu下zabbix服务器监控工具部署
  • 前端性能优化-图片
  • MapString, String循环遍历的方法
  • 快速掌握js中闭包的理解与应用(面试中如何回答闭包)
  • inkspace 0.92 安装 总结
  • js中原型,原型链的理解
  • CSS水平、垂直居中问题.md
  • Vue3写法总结
  • [译]CSS 居中(Center)方法大合集
  • 【面试系列】之二:关于js原型
  • android百种动画侧滑库、步骤视图、TextView效果、社交、搜房、K线图等源码
  • Fastjson的基本使用方法大全
  • java中的hashCode
  • js作用域和this的理解
  • Redis学习笔记 - pipline(流水线、管道)
  • vue-cli在webpack的配置文件探究
  • 阿里云爬虫风险管理产品商业化,为云端流量保驾护航
  • 区块链分支循环
  • 适配mpvue平台的的微信小程序日历组件mpvue-calendar
  • 怎样选择前端框架
  • 2017年360最后一道编程题
  • ​LeetCode解法汇总2696. 删除子串后的字符串最小长度
  • ​软考-高级-信息系统项目管理师教程 第四版【第19章-配置与变更管理-思维导图】​
  • #QT(智能家居界面-界面切换)
  • (2)(2.4) TerraRanger Tower/Tower EVO(360度)
  • (更新)A股上市公司华证ESG评级得分稳健性校验ESG得分年均值中位数(2009-2023年.12)
  • (四)模仿学习-完成后台管理页面查询
  • (原创) cocos2dx使用Curl连接网络(客户端)
  • (原創) 如何安裝Linux版本的Quartus II? (SOC) (Quartus II) (Linux) (RedHat) (VirtualBox)
  • (转)memcache、redis缓存
  • (转载)VS2010/MFC编程入门之三十四(菜单:VS2010菜单资源详解)
  • .NET Core SkiaSharp 替代 System.Drawing.Common 的一些用法
  • .NET/C# 使窗口永不激活(No Activate 永不获得焦点)
  • .net访问oracle数据库性能问题
  • [20161101]rman备份与数据文件变化7.txt
  • [ACTF2020 新生赛]Upload 1
  • [Android]竖直滑动选择器WheelView的实现
  • [Angular] 笔记 7:模块
  • [BZOJ3757] 苹果树
  • [C++] new和delete
  • [javaSE] 看知乎学习工厂模式
  • [leetcode]Symmetric Tree
  • [office] Excel自带的编辑函数求和方法 #其他#媒体
  • [SDUT](3361) 数据结构实验之图论四:迷宫探索 ---DFS(图)
  • [UGUI]实现从一个道具栏拖拽一个UI道具到另一个道具栏
  • [Uniapp]携带参数跳转界面(两种方法)
  • [Vue的组件通讯.sync修饰]Vue中.sync的使用方法和实现的方式 代码注释
  • [动态规划][蓝桥杯 2022 省 B] 李白打酒加强版 -- 代码注释含详解