javascript的深浅拷贝

javascript的数据是如何存储的

function foo(){
    let a = {title:"hello word"};
    let b = a;
    a.title= "hi" ;
    console.log(a);//{title: "hi"}
    console.log(b);//{title: "hi"}
}
foo()

由上面的例子可以看出,把a赋给b,改变b的title值,同时a的title值也会随之改变,为啥?
要解答这个问题就要先了解,JS中数据是如何存储的:
JS内存空间分为:代码空间、栈空间、堆空间
在这里插入图片描述

  • 代码空间:代码空间主要是存储可执行代码的。
  • 栈空间:栈(call stack)指的就是调用栈,用来存储执行上下文的。(每个执行上下文包括了:变量环境、词法环境)
  • 堆空间:堆(Heap)空间,一般用来存储对象的。
    JS的数据类型有:Number、BigInt、String、Boolean、Symble、Null、Undefined、Object
    在这里插入图片描述
    更详细的讲解,请看:阮一峰的《JavaScript教程-数据类型》

前7种称为原始类型,最后一种Object称为引用类型,之所以把它们区分成两种类型,是因为它们在内存中存放的位置不同;
原始类型存放在栈空间中,具体点到执行上下文来说就是:用var定义的变量会存放在变量环境中,而用let、const定义的变量会存放在词法环境中。并且对原始类型来说存放的是值,而引用类型存放的是指针,指针指向堆内存中存放的真正内容

深拷贝和浅拷贝的区别

浅拷贝
创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型(Number、String、Boolean、Null、Undefined、Symbol(ES6 引入了一种新的原始数据类型,表示独一无二的值,最大的用法是用来定义对象的唯一属性名)),拷贝的就是基本类型的值,如果属性是引用类型(比如:Object和Array),拷贝的就是内存地址(复制的是指针,最终指针指向的内存地址都是一样的) ,所以修改新拷贝的对象会影响原对象。
在这里插入图片描述

深拷贝
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
在这里插入图片描述

常用的拷贝方式:
1. Object.assign()
Object.assign()方法可以把任意多个源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象,Object.assign 只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝。
注意:如果obj只有一层的时候,是深拷贝

2. 运算符…

let obj={
	name:"张三",
	age:"18",
	school:{
		name:"北京大学"
	}
}
let copeObj={
	...obj
}
copeObj.age=19;
copeObj.school.name="清华大学";
console.log(obj)//{name:"张三", age:"18", school:{ name:"清华大学"}}

3. Array的concat()
连接两个或多个数组,并返回已连接数组的副本

let arr = [1, 2, true, {
    name: '李四'
}];
let arr2 = arr.concat()
arr2[0] = 99;
arr2[3].name = "王二"
console.log(arr) // [1,2,true,{name:"王二"}]

修改对象会修改原对象的,但是第一层上的基本数据类型修改不会影响
4. Array的slice()
选择数组的一部分,并返回新数组

let arr = [1, 2, true, {
    name: '李四'
}];
let arr2 = arr.slice();
arr2[0] = 99;
arr2[3].name = "王二"
console.log(arr) // [1,2,true,{name:"王二"}]

5. JSON对象的parse和stringify

let arr = [1, 2, true, [
		{
		    name: '李四',
		    age:19,
		    hobby:["乒乓球","羽毛球"],
		    school:{
				name:"北京大学",
				address:"北京",
				faculty:{
					name:"计算机学院",
					number:888,
				}
			}
		},
		{
		    name: '张三',
		    age:18,
		    hobby:["足球","篮球"],
		    school:{
				name:"上海交通大学",
				address:"上海",
				faculty:{
					name:"文学院",
					number:1111,
				}
			}
		}
	]
];
let arr2 =  JSON.parse(JSON.stringify(arr));
arr2[0] = 99;
arr2[3][0].name = "王二"
arr2[3][0].hobby[0]= "排球"
arr2[3][0].school.name= "深圳大学"
arr2[3][0].school.address= "深圳"
console.log(arr)

用JSON.stringify()将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象就产生了,而且对象会开辟新的栈,实现深拷贝.

但是该方法也是有局限性的:

  1. 会忽略undefined
  2. 会忽略symbol
  3. 不能序列化函数(因为JSON.stringify()方法是将一个JavaScript值(对象或者数组)转换为一个JSON字符串,不能接受函数)
  4. 不能解决循环引用的对象
let arr = {
name:"测试数据", 
time:"2021-11-11",
person:{
		    name: '李四',
		    age:19,
		    hobby:undefined,
		    sex: Symbol('male'),
		    school:{
				name:"北京大学",
				address:"北京",
				faculty:{
					name:"计算机学院",
					number:888,
				}
			},
			getSchoolName: function(){
				console.log("学校名字");
			}
		}
	}
	//会忽略undefined及symbol
let arr2 =  JSON.parse(JSON.stringify(arr));

	
//不能解决循环引用
//arr.personCope = arr.person;
//arr.person.school = arr.personCope;//会报错
//arr.person.school.name= arr.personCope.name;
//let arr2 =  JSON.parse(JSON.stringify(arr));
console.log(arr2)
//不能序列化函数:诸如 Map, Set, RegExp, Date, ArrayBuffer 和其他内置类型在进行序列化时会丢失

5. $.extend()

将两个或更多对象的内容合并到第一个对象
定义:
在默认情况下,通过$.extend()合并操作不是递归的(浅拷贝);如果第一个对象的属性本身是一个对象或数组,那么它将完全用第二个对象相同的key重写一个属性。这些值不会被合并。然而,如果将 true 作为该函数的第一个参数,那么会在对象上进行递归的合并(深拷贝)

浅拷贝(false 默认):如果第二个参数对象有的属性第一个参数对象也有,那么不会进行相同参数内部的比较,直接将第一个对象的相同参数覆盖。

深拷贝(true):如果第二个参数对象有的属性第一个参数对象也有,还要继续在这个相同的参数向下一层找,比较相同参数的对象中是否还有不一样的属性,如果有,将其继承到第一个对象,如果没有,则覆盖。

可以拷贝函数,会忽略undefined,不会忽略Symbol

let person={
		    name: '李四',
		    age:19,
		    hobby:["乒乓球","羽毛球"],
		    school:{
				name:"北京大学",
				address:"北京",
				phone:"123456789",
				faculty:{
					name:"计算机学院",
					number:888,
				}
				
			}
		};
let person02={
		    name: '张三',
		    age:19,
		    hobby:undefined,
		    sex: Symbol('male'),
			phone:"987654321",
		    school:{
				name:"上海交通大学",
				address:"上海",
				faculty:{
					name:"计算机学院",
					number:888,
				}
			},
			getSchoolName: function(){
				console.log("学校名字");
			}
		}
		//$.extend(person, person02);
		$.extend(true,person, person02);
		console.log(person);

7. 函数库 lodash 中的lodash.cloneDeep()方法
Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库
Lodash 中文文档

为什么选择 Lodash ?
Lodash 通过降低 array、number、objects、string 等等的使用难度从而让 JavaScript 变得更简单。 Lodash 的模块化方法 非常适用于:

  1. 遍历 array、object 和 string
  2. 对值进行操作和检测
  3. 创建符合功能的函数

Lodash中十个常用的工具函数

8. 自己递归实现
原理:递归方法实现深度克隆原理:遍历对象,数组直到里面都是基本数据类型,然后再去复制,就是深度拷贝
我自己在网上随便找了几个深度拷贝的方法

 /* ------------------------- 方法一 ------------------------- */
    //深度拷贝一个数组或者对象
    deepCope(obj) {
      let type = Object.prototype.toString.call(obj);
      if (type == "[object Array]") {
        let backObj = [];
        for (let val of obj) {
          backObj.push(this.deepCope(val));
        }
        return backObj;
      }
      if (type == "[object Object]") {
        let backObj = {};
        for (let key in obj) {
          if (obj.hasOwnProperty(key)) {
            backObj[key] = this.deepCope(obj[key]);
          }
        }
        return backObj;
      }
      return obj;
    },

    /* ------------------------- 方法二 ------------------------- */
    deepCopy(obj2) {
      //递归深层copy
      /**
       * 把一个对象递归拷贝给另外一个对象
       * 源对象与拷贝后的对象没有引用关系
       */
      var obj = this.isArray(obj2) ? [] : {};
      for (var property in obj2) {
        // 如果当前拷贝的数据还是一个对象的话,那么继续调用
        // deepCopy 进行二次拷贝
        // 递归
        if (this.isObject(obj2[property])) {
          obj[property] = this.deepCopy(obj2[property]);
        } else {
          obj[property] = obj2[property];
        }
      }
      return obj;
    },
    isArray(val) {
      //检测数组
      //通过Object.prototype.toString.call来精准检测类型
      return Object.prototype.toString.call(val) === "[object Array]";
    },
    isObject(val) {
      //检测对象
      return typeof val === "object" && val !== null;
    },

    /* ------------------------- 方法三 ------------------------- */
    deepCopy(data) {
      if (data.constructor.name === "Array") {
        // 判断为数组类型
        var arrCopy = [];
        for (let i = 0, len = data.length; i < len; i++) {
          //遍历数组
          if (data[i] instanceof Object) {
            arrCopy.push(this.deepCopy(data[i]));
          } else {
            // 基本类型
            arrCopy.push(data[i]);
          }
        }
        return arrCopy;
      } else {
        // 为对象
        var objCopy = {};
        for (let x in data) {
          if (data[x] instanceof Object) {
            objCopy[x] = this.deepCopy(data[x]);
          } else {
            // 基本类型
            objCopy[x] = data[x];
          }
        }
        return objCopy;
      }
    },

深拷贝思路是:

  1. 处理原始类型 如: Number String Boolean Symbol Null Undefined
  2. 处理不可遍历类型 如: Date RegExp Function
  3. 处理循环引用情况 使用: WeakMap
  4. 处理可遍历类型 如: Set Map Array Object
    详细的深度拷贝实现思路:https://juejin.cn/post/6881889117437689864
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值