JS的深拷贝和浅拷贝

JS的浅拷贝和深拷贝是绕不过去的一环节,今天就做个整理.

一.JS的数据类型

要介绍深浅拷贝,首先先了解一下数据类型.

数据类型分为两种:

  • 基本数据类型
  • 引用数据类型

1.1 基本数据类型

基本类型常见6种

  • String
  • Boolean
  • Number
  • Undefined
  • null
  • symbol (es6新增)

1.2 引用数据类型

引用数据类型主要3种

  • Object
  • Array
  • Function

还有其他引用类型,包括Date、RegExp、Map、Set等

1.3 两者区别

两种类型区别在于:存储位置的不同

  1. 内存地址分配:
  • 基本数据类型:将值存储在栈中 ,栈中存放的是对应的值
  • 引用数据类型:将对应的值存储在堆中,栈中存放的是指向堆内存的地址
  1. 赋值变量:
  • 基本数据类型: 是生成相同的值,两个对象对应不同的地址
  • 引用数据类型: 是将保存对象的内存地址赋值给另一个变量。也就是两个变量指向堆内存中同一个对象

1.3.1 基本类型赋值方式

let a = 10;
let b = a; // 赋值操作
b = 20;
console.log(a); // 10值

a是基本类型,存储在栈中;把a赋值给b,虽然两个变量的值相等,但是两个变量保存了两个不同的内存地址

在这里插入图片描述

1.3.2 引用类型赋值方式

var obj1 = {}
var obj2 = obj1;
obj2.name = "Xxx";
console.log(obj1.name); // xxx

obj1是引用类型,将数据存放在堆内存中,而栈中存放的是内存地址.在obj1赋值给obj2,实际是将obj1的引用地址复制了一份给了obj2,实际上他们共同指向了同一个堆内存对象,所以更改obj2会对obj1产生影响

在这里插入图片描述

二. 浅拷贝

说完数据类型,开始介绍浅拷贝.

2.1 定义

浅拷贝是指创建新的数据,这个数据有着原始数据属性值的一份精确拷贝.

更明确来说:

  • 基本数据类型拷贝的是基本类型的值
  • 引用数据类型拷贝的是内存地址

更简单来说:

  • 浅拷贝就是拷贝一层,引用类型的深层次还是共享内存地址(详细可见第一部分的例子)

2.2 js 常见的浅拷贝方法

浅拷贝方法还是很多,这里就列举几个面试爱问的浅拷贝方法吧

  • Object.assign()
  • 扩展运算符
  • Array.concat()
  • Array.slice()

2.2.1 Object.assign()

let oldObj = {
    inObj: {a: 1, b: 2}
  }
  let newObj = Object.assign({}, oldObj)
  newObj.inObj_2 = {a: 1, b: 2}
  
  newObj.inObj.a = 2
  
  console.log(oldObj) // { inObj: { a: 2, b: 2 } }
  console.log(newObj) // { inObj: { a: 2, b: 2 }, inObj_2: { a: 1, b: 2 } }

2.2.2 扩展运算符

let oldObj = {
    inObj: {a: 1, b: 2}
  }
  let newObj = {...oldObj}
  newObj.inObj_2 = {a: 1, b: 2}
  newObj.inObj.a = 2
  console.log(oldObj) // { inObj: { a: 2, b: 2 } }
  console.log(newObj) // { inObj: { a: 2, b: 2 }, inObj_2: { a: 1, b: 2 } }
const oldArr = ["One", "Two", "Three",{name:"Four"}]
const newArr = [...oldArr]
newArr[1] = "love";
newArr[3].name = 'Five'
console.log(oldArr) //  ['One', 'Two', 'Three', { name: 'Five' } ]
console.log(newArr) // [ 'One', 'love', 'Three', { name: 'Five' } ]

2.2.3 Array.concat()

const oldArr = ["One", "Two", "Three",{name:"Four"}]
const newArr = oldArr.concat(["Six"])
newArr[1] = "love";
newArr[3].name = 'Five'
console.log(oldArr) //  ['One', 'Two', 'Three', { name: 'Five' } ]
console.log(newArr) // [ 'One', 'love', 'Three', { name: 'Five' }, 'Six'  ]

2.2.4 Array.slice()

const oldArr = ["One", "Two", "Three",{name:"Four"}]
const newArr = oldArr.slice(0)
newArr[1] = "love";
newArr[3].name = 'Five'
console.log(oldArr) //  ['One', 'Two', 'Three', { name: 'Five' } ]
console.log(newArr) // [ 'One', 'love', 'Three', { name: 'Five' } ]

三.深拷贝

3.1 定义

深拷贝开辟一个新的栈,两个对象属性相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性

简单来说:就是完全独立的双胞胎,彼此之间不会影响

3.2 js 常见的深拷贝方法

  • _.cloneDeep()
  • JSON.stringify()
  • 手写递归方法

3.2.1 _.cloneDeep()


const _ = require('lodash');
let oldObj = {
    a: 1, b: 2,c:{value:3}
}
let newObj = _.cloneDeep(oldObj)
newObj.a = 2
newObj.c.value = 4
console.log(oldObj) // { a: 1, b: 2, c: { value: 3 } }
console.log(newObj) // { a: 2, b: 2, c: { value: 4 } }

3.2.2 JSON.stringify()

const oldObj = {
    name: 'A',
    name1: {value:3}
}
const newObj = JSON.parse(JSON.stringify(oldObj));
newObj.name = 'B'
newObj.name1.value = 4
console.log(oldObj) 
// {name: 'A',name1: { value: 3 }}
console.log(newObj) 
// { name: 'B', name1: { value: 4 } }
3.2.2.1 JSON.stringify() 深拷贝弊端

先看个例子:

const map = new Map()
map.set('arr',['a'])
const oldObj = {
    name1: 'A',
    name2: undefined,
    name3: function() {},
    name4:  Symbol('A'),
    date: new Date('1990/01/01'),
    type: NaN,
    type1: Infinity,
    type2: null,
    map: map,
    reg: new RegExp('\\w+'),
    err: new Error('error message')
}
const newObj = JSON.parse(JSON.stringify(oldObj)); 
console.log(map.propertyIsEnumerable('arr'))// false
// {name1: 'A',name2: undefined,name3: [Function: name3],name4: Symbol(A),date: 1989-12-31T16:00:00.000Z,type: NaN,type1: Infinity,type2: null,map: Map { 'arr' => [ 'a' ],reg:new RegExp('\\w+'),err:Error: error message }}
console.log(oldObj) 
// {name1: 'A',date: '1989-12-31T16:00:00.000Z',type: null,type1: null,type2: null,map: {},reg:{},err:{} }}
console.log(newObj) 
var obj1 = {
    x: 1, 
    y: 2
  };
  obj1.z = obj1;
  var obj2 = JSON.parse(JSON.stringify(obj1)); //  Converting circular structure to JSON 栈溢出,抛出错误

从上面例子能看出,JSON.stringify() 会存在以下问题:

  • 忽略undefined、symbol和函数
  • Date 日期调用了 toJSON() 将其转换为了 string 字符串(Date.toISOString()),因此会被当做字符串处理
  • NaN 和 Infinity 格式的数值及 null 都会被当做 null
  • RegExp、Error对象会返回空;
  • 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性。
  • 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
3.2.2.2 JSON.stringify() 深拷贝的原理

原理: JSON.parse(JSON.stringify())方法本质就是先将js对象进行序列化,然后再反序列化还原js对象.

那么什么是序列化?

  • 简单说法: 将对象转为字符串
  • 高端说法: 将对象的状态信息转换为可以存储或传输的形式的过程(百度的)

为什么需要序列化?

  • 对象本身存储的是一个地址映射,简单的说,对象obj就是我们的程序在电脑通电时在内存中维护的一种东西,如果我们程序停止了或者电脑断电了,对象obj将不复存在。所以需要把对象obj序列化转换成一个字符串的形式,然后再保存在磁盘上。(也就是存储).
  • 当我们通过HTTP协议把对象obj的内容发送到客户端时候,还是需要先把对象obj序列化,然后客户端根据接收到的字符串再反序列化解析出相应的对象.。(也就是传输).
3.2.2.3 JSON.stringify() 深拷贝产生弊端缘由

JSON.stringify() 时候使用的JSON的语法,它并不能表示JavaScript中的所有值,对于JSON语法不支持的属性,序列化后会将其省略。其详细规则如下:

  • 对于JavaScript中的五种原始类型,JSON语法支持数字、字符串、布尔值、null四种,不支持undefined;

  • NaN、Infinity和-Infinity序列化的结果是null;

  • JSON语法不支持函数;

  • 除了RegExp、Error对象,JSON语法支持其他所有对象;

  • 日期对象序列化的结果是ISO格式的字符串,但JSON.parse()依然保留它们字符串形态,并不会将其还原为日期对象;

  • JSON.stringify()只能序列化对象的可枚举的自有属性;

3.2.3 手写递归方法

function cloneDeep(obj, hash = new WeakMap()){
    if(obj instanceof Date) return new Date(obj);
    if (obj instanceof RegExp) return new RegExp(obj);
    if(!obj || typeof obj !== 'object') return obj;
    if(hash.get(obj)) return hash.get(obj);
    // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
    let newObj= new obj.constructor();
    hash.set(obj,newObj);
    for(let key in obj) {
        if(obj.hasOwnProperty(key)){
            newObj[key] = cloneDeep(obj[key],hash)
        }
    }
    return newObj
}

测试例子

const oldObj = {
    name: 'A',
    name1: {value:{type:'obj'}},
    name2: undefined,
    name3: function() {},
    name4:  Symbol('A'),
    type: NaN,
    type1: Infinity,
    type2: null,
    reg: new RegExp('\\w+'),
    err: new Error('error message')
}
oldObj.z = oldObj
const newObj = cloneDeep(oldObj)
newObj.name1.value.type = 'Number'

console.log(oldObj) 
// {name: 'A',name1: { value: { type: 'obj' } },name2: undefined,name3: [Function: name3],name4: Symbol(A),type: NaN,type1: Infinity,type2: null,reg: /\w+/,err: Error: error message, z: [Circular]
console.log(oldObj) }

console.log(newObj) 
// {name: 'A',name1: { value: { type: 'Number' } },name2: undefined,name3: [Function: name3],name4: Symbol(A),type: NaN,type1: Infinity,type2: null,reg: /\w+/,err: Error: error message, z: [Circular]
console.log(oldObj) }


const oldArr = ["One", "Two", "Three",{name:"Four"}]
const newArr = cloneDeep(oldArr)
newArr[2] = 'Five'
newArr[3].name = 'Six'

console.log(oldArr) // [ 'One', 'Two', 'Three', { name: 'Four' } ]
console.log(newArr) // [ 'One', 'Two', 'Five', { name: 'Six' } ]

3.2.2.1 手写递归弊端
  1. 需要考虑多种数据类型,不仅有Object,还有Map,Set等数据结构(上面代码不支持Map/Set这种)
  2. 需要考虑拷贝对象的循环引用
  3. 使用递归的方式可能会造成爆栈,解决办法就是采用迭代的方式(网上很多材料,就不写了,放一个链接大家可供参考迭代方法 )

四. 小结

  1. 浅拷贝和深拷贝都能创建出一个新的对象,
  • 但浅拷贝在复制引用类型时,只复制引用地址,新旧对象还是共享同一块内存,修改对象属性会影响原对象;
  • 而深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象
  1. 浅拷贝常见方法
  • Object.assign()
  • 扩展运算符
  • Array.concat()
  • Array.slice()

3.深拷贝常见方法

  • _.cloneDeep()
  • JSON.stringify()
  • 手写递归方法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值