前言
在面试或准备面试的过程中想必大家常常会看到或者被问到深浅拷贝的问题,探究深浅拷贝这就涉及到数据的不同类型以及内存中所存放位置。 所谓深浅拷贝,其实都是数据的复制行为,其主要区别在于复制出来的新对象和原对象是否会相互影响...
那就一起来了解一下吧 😀
数据类型
- 基本数据类型 -- 在栈内存值
- 引用数据类型 -- 在栈内存址,该地址指向对象在堆内存中的位置
栈内存:是一种特殊的线性表,它具有后进先出的特性,存放基本类型。
堆内存:存放引用类型(在栈内存中存一个基本类型值保存对象在堆内存中的地址,用于引用这个对象)。
浅拷贝
浅拷贝 指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝
- 基本数据类型:进行简单的值拷贝
- 复杂数据类型:拷贝的是内存地址, 即浅拷贝是拷贝一层地址,深层次的引用类型则共享内存地址
浅拷贝的场景
Object.assgin
Object.assign() 主要用于对象合并,将源对象中的属性复制到目标对象中,他将返回目标对象。
原对象属性中包含引用类型:进行了浅拷贝,拷贝了原对象属性值,所以拷贝的对象改变的时候原对象的引用类型也改变
var obj = {
age: 18,
nature: ['smart', 'good'],
names: {
name1: 'fx',
name2: 'xka'
},
love: function () {
console.log('fx is a great girl')
}
}
var newObj = Object.assign({}, obj);
newObj.names.name1='haha'
console.log('newObj',newObj.names) // newObj { name1: 'haha', name2: 'xka' }
console.log('obj',obj.names) // obj { name1: 'haha', name2: 'xka' }
对于数组的浅拷贝
Array.prototype.slice(), Array.prototype.concat()
slice()
var arr = ['jack',25, {hobby:'tennise'}];
let arr1 = arr.slice()
arr1[2].hobby='football'
arr1[0]='rose'
console.log( 'arr', arr) // arr [ 'jack', 25, { hobby: 'football' } ]
console.log( 'arr1', arr1) // arr1 [ 'rose', 25, { hobby: 'football' } ]
concat()
var arr2 = ['jack',25, {hobby:'tennise'}];
let arr3=arr2.concat()
arr2[2].hobby = 'basketball'
arr2[0]='rose'
console.log( 'arr2', arr2) // arr2 [ 'rose', 25, { hobby: 'basketball' } ]
console.log( 'arr3', arr3) // arr3 [ 'jack', 25, { hobby: 'basketball' } ]
拓展运算符(解构)
使用拓展运算符实现的复制 -- 解构
let aa = {
age: 18,
name: 'aaa',
address: {
city: 'shanghai'
}
}
let bb = {...aa};
bb.name="bb"
bb.address.city = 'hangzhou';
console.log(aa); // { age: 18, name: 'aaa', address: { city: 'hangzhou' } }
console.log(bb); // { age: 18, name: 'bb', address: { city: 'hangzhou' } }
手写浅拷贝
将对象上的所有属性拷贝赋值到新的对象上,仅仅是值的拷贝
function shadowClone(obj){
const newObj ={}
// 将对象上的属性拷贝赋值
for( let prop in obj){
newObj[prop] = obj[prop]
}
return newObj
}
深拷贝
深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
常见的深拷贝
_.cloneDeep()
借助第三方 lodash 库,实现对象的深度拷贝,拷贝后的属性互不干扰
首先需要引入 lodash.js
const _ = require('lodash');
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false
JSON.stringify() & JSON.prase()
原理就是用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,生成新的对象,对象会开辟新的栈。
const obj2=JSON.parse(JSON.stringify(obj1));
但是通过JSON转换的方式存在弊端,并不是所有的数据类型的转换JSON都是支持的,因此部分不支持的就会被忽略掉。
- 当对象里面有
undefined、函数、Symbol时,经过JSON.stringify的深拷贝后会消失。 - 会忽略 undefined、symbol。
- 数组中的
undefined、函数、Symbol值会被转化为 null - 不能解决循环引用的问题,直接报错。
// 数组中的 undefined 会省略
const obj = {
name: 'A',
name1: undefined,
name3: function() {},
name4: Symbol('A')
}
const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {name: "A"}
// 数组中的 undefined 会转化为 null
const arr = [1,2,3,undefined, 4]
const arr2 = JSON.stringify(arr)
console.log(arr2) // '[1,2,3,null,4]'
jQuery.extend()
const $ = require('jquery');
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
const obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false
手写深拷贝
通过 WeakMap 缓存、递归对象中的各个属性实现深拷贝 ,WeakMap 保存对对象的弱引用,解决循环引用的问题。
原生的 WeakMap 持有的是每个键对象的“弱引用”,这意味着在没有其他引用存在时垃圾回收能正确进行。原生 WeakMap 的结构是特殊且有效的,其用于映射的 key 只有在其没有被回收时才是有效的。
// hash = new WeakMap() 使用 WeakMap 缓存
function cloneDeep(obj, hash = new WeakMap()){
// 如果 为null 或是 原始值 不进行拷贝
if( obj === null || typeof obj !== 'object') return obj
// 如果是 日期或者正则 这直接创建新的对象返回
if( obj instanceof Date) return new Date(obj)
if( obj instanceof RegExp) return new RegExp(obj)
// hash 判断防止循环引用情况
if(hash.get(obj)) return hash.get(obj)
// 创建一个和obj类型一样的对象
let cloneObj = new obj.constructor() //找到对象 obj 的构造函数 创建 新的实例对象存放属性
hash.set(obj, cloneObj) // 缓存创建的对象
// 遍历 obj 中的每一项,能遍历数组即对象,当 obj 为数组时 prop 为数组的下标
for(let prop in obj){
// 判断是否为对象自身的属性
if(obj.hasOwnProperty(prop)){
// 实现递归拷贝,将缓存的 hash 作为参数传入
cloneObj[prop] = cloneDeep(obj[prop], hash)
}
}
return cloneObj
}
const obj3 = {
name: 'Jack',
address: {
x: 100,
y: 200
}
}
let copyObj = cloneDeep(obj3)
console.log(copyObj)
console.log(copyObj.address === obj3.address) //false
使用一个 WeakMap 结构存储被拷贝的对象,每一次进行拷贝的时候就先向 WeakMap 查询该对象是否已经被拷贝,如果已经被拷贝则取出该对象并返回。
Map 与 WeakMap
- 使用map,对象会占用内存,可能不会被垃圾回收。Map对一个对象是强引用,可能会导致内存泄漏
- Weakmap 不会阻止关键对象的垃圾回收,只接受object作为key,且只保存对对象的弱引用
对象的遍历
在这里顺便整理一下遍历对象常用的几个方法:
- for ... in
-
- for in遍历对象键值(key)
- 循环遍历对象自身的和继承的可枚举属性
- 不含Symbol属性
- Object.keys(obj) / Object.values(obj)
-
- 由对象的 key / value 组成的数组
- 包括对象自身所有可枚举属性
- 不含继承属性、Symbol属性
- Object.getOwnPropertyNames(obj)
-
- 返回一个数组
- 包含对象自身所有属性
- 包括不可枚举属性、但不包含 Symbol 属性
- 使用Reflect.ownKeys(obj)遍历
-
- 返回一个数组
- 包含对象自身的所有属性,不管属性名是Symbol或字符串,也不管是否可枚举。
对象Reflect包含用于调用可拦截 JavaScript 对象内部方法的静态方法
深拷贝、浅拷贝
浅拷贝和深拷贝都创建出一个新的对象,但在复制对象属性的时候,行为就不一样:
浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象。
深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象
小结
在对复杂类型进行拷贝时:
浅拷贝: 只会拷贝一层,属性为对象时 浅拷贝是复制值,两个对象指向同一块内存地址。
深拷贝: 进行递归深层拷贝,属性为对象时,深拷贝开辟新的空间存放对象,两个对象指向不同的地址。