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.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 手写递归弊端
- 需要考虑多种数据类型,不仅有Object,还有Map,Set等数据结构(上面代码不支持Map/Set这种)
- 需要考虑拷贝对象的循环引用
- 使用递归的方式可能会造成爆栈,解决办法就是采用迭代的方式(网上很多材料,就不写了,放一个链接大家可供参考迭代方法 )
四. 小结
- 浅拷贝和深拷贝都能创建出一个新的对象,
- 但浅拷贝在复制引用类型时,只复制引用地址,新旧对象还是共享同一块内存,修改对象属性会影响原对象;
- 而深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象
- 浅拷贝常见方法
- Object.assign()
- 扩展运算符
- Array.concat()
- Array.slice()
3.深拷贝常见方法
- _.cloneDeep()
- JSON.stringify()
- 手写递归方法