深拷贝与浅拷贝
浅拷贝
拷贝的是对象的指针,修改内容会互相影响
如果属性是基本类型,拷贝的就是基本类型的值
如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象
简单赋值
即简单的将一个对象赋值给另外一个对象
let obj1 = {
a: 1,
b: 2
}
let obj2 = obj1
obj2.b = 3
console.log(obj1) // { a: 1, b: 3 }
console.log(obj1 === obj2) //true
Object.assign
Object.assign() 只复制属性值,如果属性值是一个对象的话,那么只会引用这个对象的指针
let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }
loadash的_.clone方法
var objects = [{ 'a': 1 }, { 'b': 2 }];
var shallow = _.clone(objects);
console.log(shallow[0] === objects[0]); //true
展开运算符...
与assign一样,对于属性值是对象的属性,他只会引用其属性值的指针
let obj1 = {
a: 1,
c: {
d: 4
}
}
let obj2 = {...obj1}
console.log(obj1 === obj2) //false
console.log(obj2.c === obj1.c); //true
concat方法
concat() 方法用于合并两个或多个数组
此方法不会更改现有数组,而是返回一个新数组
let arr = [1, 3, {
username: 'kobe'
}];
let arr2 = arr.concat();
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]
slice方法
slice() 方法返回一个新的数组对象
这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括end),原始数组不会被改变
let arr = [1, 3, {
username: ' kobe'
}];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr); // [ 1, 3, { username: 'wade' } ]
深拷贝
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
使用JSON.stringify
lodash的_.cloneDeep方法
手写
基础
首先,我们先写出一个能够实现浅拷贝的函数
思路:就是使用循环将原对象上的属性一个一个添加到新的克隆对象上
function clone(target) {
//创建一个克隆的对象
let cloneTarget = {}
//使用for...in把所有需要克隆的属性都添加到cloneTarget中
for(const key in target){
cloneTarget[key] = target[key]
}
//返回克隆对象
return cloneTarget
}
在深拷贝中,我们需要解决的第一个问题就是需要考虑拷贝的深度,我们目前只是拷贝到第一层,所以我们可以采用递归的方式来解决
- 如果是原始数据,则直接返回即可
- 如果是引用类型,则需要创建一个新的对象,遍历需要克隆的对象,添加属性值,如果又遇到引用类型则递归即可
function clone(target) {
//判断是不是引用类型,是的话则需要递归生成新对象
if(typeof target === 'object'){
//创建一个克隆的对象
let cloneTarget = {}
//使用for...in把所有需要克隆的属性都添加到cloneTarget中
for(const key in target){
//对每一个属性值进行检查,是引用类型则进一步递归遍历处理
cloneTarget[key] = clone(target[key])
}
//返回克隆对象
return cloneTarget
}else{
//是原始类型则直接返回
return target
}
}
考虑数组
现在的代码只适用于对象,但是数组与对象同样都是引用数据类型,所以可以用同样的思路实现数组深拷贝
思路:将克隆出来的产物cloneTarget根据数据类型产出即可
function clone(target) {
if(typeof target === 'object'){
//创建一个克隆的对象或数组
let cloneTarget = Array.isArray(target) ? [] : {}
for(const key in target){
cloneTarget[key] = clone(target[key])
}
return cloneTarget
}else{
return target
}
}
循环引用
这个问题也是存在于JSON.stringify()这个方法中
循环引用也就是自身引用了自身,例如:
const obj = {a: 1, b: 2}
obj.obj = obj //自身引用了自身
此时如果用我们之前的函数去拷贝这样一个对象,那么就会出现爆栈的情况
Uncaught RangeError: Maximum call stack size exceeded
解决思路:可以先开辟一块内存空间,存储当前对象和拷贝对象的对应关系,需要拷贝对象的时候,可以先去存储空间中找,如果有这个对象则直接返回,如果没有则继续拷贝
现在这个存储空间,由于存储的是键值对关系的集合,所以我们可以考虑使用**Map数据结构**,拷贝对象之前先在这个Map数据结构中寻找即可
- 找到:直接返回
- 找不到:将当前对象和克隆对象存储起来,然后继续克隆
function clone(target, map = new Map()) {
if(typeof target === 'object'){
let cloneTarget = Array.isArray(target) ? [] : {}
//检查该对象有没有被克隆过
if(map.get(target)){
//如果被克隆过则直接返回该对象
return target
}
//如果没有的话则需要新建键值对存储起来
map.set(target, cloneTarget)
for(const key in target){
//并且把当前的map数据结构传递给下一个递归过程
cloneTarget[key] = clone(target[key], map)
}
return cloneTarget
}else{
return target
}
}
到这里,我们的代码已经能够解决循环引用的问题了,那么现在我们要对其优化一下
我们使用了Map数据结构进行存储,我们可以改用WeakMap数据结构代替Map
WeakMap的特点:
WeakMap对象是一组键/值对的集合,其中的键是弱引用的其键必须是对象,而值可以是任意的
弱引用:
在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收
如果我们现在创建一个对象{},那么只有我们手动将其赋值为null的时候,才会被垃圾回收机制进行回收
而弱引用对象,垃圾回收机制则会自动帮我们回收
所以当我们拷贝的对象十分庞大的时候,使用Map会造成很大的额外消耗,需要我们手动清除其属性才能释放这块内存,而使用WeakMap则可以自动帮我们回收垃圾
性能优化
在遍历中,我们使用了for...in循环,但实际上,for...in循环的效率是最低的,而**while循环和for循环的效率最高**,所以此处采用while循环
for...in循环效率最低:
for...in不仅会遍历数组的元素,而且还会去每一个元素的原型上面寻找属性for...in还要判断一个属性是不是枚举属性,是的话则不遍历for...in还会要求按特定的顺序输出属性,先数字,再按字典顺序输出string属性(不会获取Symbol属性)
首先,我们新建一个遍历函数去模拟for...in循环
function forEach(array, iteratee) {
//设置索引
let index = -1
//获取数组长度
const length = array.length
//使用while循环代替for...in
while (++index < length) {
//由于for...in能自动提取出键,所以我们要模拟他的功能
//此处我们传入一个回调函数即可,参数将数组的项和索引传进入
iteratee(array[index], index)
}
//将原数组返回
return array
}
然后,修改clone函数中的循环逻辑即可
function clone(target, map = new WeakMap()) {
if (typeof target === 'object') {
//记录传入的是数组还是对象
const isArray = Array.isArray(target)
let cloneTarget = isArray ? [] : {}
if (map.get(target)) {
return target
}
map.set(target, cloneTarget)
//如果是对象,要为他创建对应的keys集合
const keys = isArray ? undefined : Object.keys(target)
//使用我们的forEach去代替for...in优化性能,将属性拷贝到克隆对象中
forEach(keys || target, (value, key) => {
//如果是对象的话,我们需要指定键为value,不然此处遍历出来的是数字
if (keys) key = value
//对每一个属性值进行检查,是引用类型则进一步递归遍历处理
//并且把当前的map数据结构传递给下一个递归过程
cloneTarget[key] = clone(target[key], map)
})
return cloneTarget
} else {
return target
}
}
测试用例:
const target = {
field1: 1 ,
field2: undefined,
field3: { child: 'child'},
field4: [2, 4, 8],
f: { f: { f: { f: { f: { f: { f: { f: { f: { f: { f: { f: {} } } } } } } } } } } },
};
target.target = target;
获取对象准确的类型
在上述的克隆函数中,我们只考虑了object和array两种数据类型
而在判断object的时候,我们只使用了typeof去检查一个对象,但是这样的检查往往是不严谨的,因为没有考虑到null、function,所以我们要重新判断object
此处将判断object的代码提取出来,作为一个**isObject函数**
function isObject(target) {
//检查target的类型
const type = typeof target
//考虑null和function的情况
return target !== null && (type === 'object' || type === 'function')
}
然后再修改一下clone函数即可
function clone(target, map = new WeakMap()) {
//判断是不是引用类型,是的话则需要递归生成新对象
if (isObject(target)) {
/** 省略 */
} else {
//是原始类型则直接返回
return target
}
}
考虑其他数据类型
我们可以使用toString方法来获取准确的引用类型
每一个引用类型都有toString方法,因为这个方法被每个Object对象继承,会返回[[object type]],但是很有可能这个方法在自定义对象中被覆盖,导致不能获取到对象类型type
既然如此,我们可以直接调用Object原型上的toString方法,再修改一个this指向即可
我们将这个提取引用类型的逻辑提取出来:
function getType(target) {
//调用Object原型对象上的toString
return Object.prototype.toString.call(target)
}
这样每一个对象都可以正确的判断出他的类型,如下表:
| 调用 | 结果 |
|---|---|
Object.prototype.toString.call(true) | [object Boolean] |
Object.prototype.toString.call(123) | [object Number] |
Object.prototype.toString.call('aaa') | [object String] |
Object.prototype.toString.call(null) | [object Null] |
Object.prototype.toString.call(undefined) | [object undefined] |
Object.prototype.toString.call(Symbol()) | [object Symbol] |
Object.prototype.toString.call({}) | [object Object] |
Object.prototype.toString.call(function(){}) | [object Function] |
Object.prototype.toString.call([]) | [object Array] |
Object.prototype.toString.call(new Error()) | [object Error] |
Object.prototype.toString.call(new ReqExp()) | [object ReqExp] |
Object.prototype.toString.call(Math) | [object Math] |
Object.prototype.toString.call(JSON) | [object JSON] |
Object.prototype.toString.call(window) | [object golbal] |
为了后续使用方便,我们抽取其中的一些常用数据类型:
//可以继续遍历的类型
const mapTag = '[object Map]'
const setTag = '[object Set]'
const arrayTag = '[object Array]'
const objectTag = '[object Object]'
//不可以继续遍历的类型
const boolTag = '[object Boolean]'
const dateTag = '[object Date]'
const errorTag = '[object Error]'
const numberTag = '[object Number]'
const stringTag = '[object String]'
const regexpTag = '[object RegExp]'
const symbolTag = '[object Symbol]'
处理可继续遍历的类型
可继续遍历的类型:内存中还可以存储引用数据类型的数据
我们已经实现object和Array这两个可继续遍历的类型了,另外我们再实现以下Map和Set这两个数据结构
现在我们就需要根据传入的target判断数据结构,然后再根据数据类型创建克隆对象
这里我们可以通过一个小技巧:使用constructor方式获取,就可以获取到它的构造函数,再使用这个构造函数进行创建实例,就可以创建出新的克隆对象,而且这个对象还可以保留原型对象上的数据
function getInit(target){
const Ctor = target.constructor
return new Ctor()
}
修改克隆函数,加上处理map、set的逻辑
function clone(target, map = new WeakMap()) {
//判断是不是引用类型,不是的话则直接返回
if (!isObject(target)) return target
const isArray = Array.isArray(target)
//获取当前数据的详细类型
const type = getType(target)
//创建克隆对象
let cloneTarget
//检查是不是可以继续遍历的类型
if (deepTag.includes(type)) {
//创建对应的数据类型实例赋值给克隆对象
cloneTarget = getInit(target)
}
if (map.get(target)) {
return target
}
map.set(target, cloneTarget)
//克隆Set数据结构
if (type === setTag) {
target.forEach(value => {
//往克隆对象中添加每一个值,并检查是不是引用数据类型
cloneTarget.add(clone(value))
})
//返回克隆的Set数据
return cloneTarget
}
//克隆Map数据结构
if (type === mapTag) {
target.forEach((value, key) => {
//往克隆对象中添加每一个值,并检查是不是引用数据类型
cloneTarget.set(key, clone(value))
})
//返回克隆的Map数据
return cloneTarget
}
const keys = isArray ? undefined : Object.keys(target)
forEach(keys || target, (value, key) => {
if (keys) key = value
cloneTarget[key] = clone(target[key], map)
})
return cloneTarget
}
测试用例:
const map = new Map([['aaa', 'AAA'], ['bbb', 'BBB']])
const set = new Set(['ccc', 'ddd'])
const target = {
field1: 1,
field2: undefined,
field3: { child: 'child' },
field4: [2, 4, 8],
empty: null,
map,
set,
}
const obj = clone(target)
target.map.set('hhh', '哈哈哈')
target.set.add('hhh')
console.log(obj, target);
处理不可继续遍历的类型
剩余的类型我们将其处理为不可继续遍历的类型:
function cloneOtherType(target, type) {
const Ctor = target.constructor
switch (type){
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(target)
default:
return null
}
}
克隆函数
虽然克隆函数并没有什么实际的应用场景,两个对象使用同一个地址中的函数也是没有问题的,所以一般不用处理
在函数库lodash库中就没有对其做处理,而是直接返回
但是如果我们一定要克隆一个函数,也不是没有办法的
首先,函数分为两种:普通函数和箭头函数,而两者可以用有无prototype属性来区分
先处理普通函数:使用正则取出函数体和参数,然后使用new Function()构建即可
再处理箭头函数:直接使用eval函数即可
function cloneFunction(func){
//获取函数体的正则判断
const bodyReg = /(?<={)([\s\S]*)(?=})/
//获取参数的正则判断
const paramReg = /(?<=().+(?=))/
//将函数转化成字符串
const funcString = func.toString()
//判断有没有prototype,有的话则为普通函数
if(func.prototype){
//提取参数
const param = paramReg.exec(funcString)
//提取函数体
const body = bodyReg.exec(funcString)
//如果有函数体
if(body){
//有参数则处理参数在构建函数,没有则直接构建
if(param){
const paramArr = param[0].split(',')
return new Function(...paramArr, body[0])
}else{
return new Function(body[0])
}
}else{
//没有函数体则返回null
return null
}
}else{
//处理箭头函数
return eval(funcString)
}
}
最终版本
//可以继续遍历的类型
const mapTag = '[object Map]'
const setTag = '[object Set]'
const arrayTag = '[object Array]'
const objectTag = '[object Object]'
const deepTag = [mapTag, setTag, arrayTag, objectTag]
//不可以继续遍历的类型
const boolTag = '[object Boolean]'
const dateTag = '[object Date]'
const errorTag = '[object Error]'
const numberTag = '[object Number]'
const stringTag = '[object String]'
const regexpTag = '[object RegExp]'
const symbolTag = '[object Symbol]'
const funcTag = '[object Function]'
//遍历函数,模拟for...in
function forEach(array, iteratee) {
//设置索引
let index = -1
//获取数组长度
const length = array.length
//使用while循环代替for...in
while (++index < length) {
//由于for...in能自动提取出键,所以我们要模拟他的功能
//此处我们传入一个回调函数即可,参数将数组的项和索引传进入
iteratee(array[index], index)
}
//将原数组返回
return array
}
//判断target的类型是不是object
function isObject(target) {
//检查target的类型
const type = typeof target
//考虑null和function的情况
return target !== null && (type === 'object' || type === 'function')
}
//获取数据类型
function getType(target) {
//调用Object原型对象上的toString
return Object.prototype.toString.call(target)
}
//创建克隆对象
function getInit(target) {
//获取初始化数据target的构造函数
const Ctor = target.constructor
//构造新的实例
return new Ctor()
}
//处理不可继续遍历的类型
function cloneOtherType(target, type) {
const Ctor = target.constructor
switch (type){
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(target)
case funcTag:
return cloneFunction(target)
default:
return null
}
}
//克隆函数
function cloneFunction(func){
//获取函数体的正则判断
const bodyReg = /(?<={)([\s\S]*)(?=})/
//获取参数的正则判断
const paramReg = /(?<=().+(?=))/
//将函数转化成字符串
const funcString = func.toString()
//判断有没有prototype,有的话则为普通函数
if(func.prototype){
//提取参数
const param = paramReg.exec(funcString)
//提取函数体
const body = bodyReg.exec(funcString)
//如果有函数体
if(body){
//有参数则处理参数在构建函数,没有则直接构建
if(param){
const paramArr = param[0].split(',')
return new Function(...paramArr, body[0])
}else{
return new Function(body[0])
}
}else{
//没有函数体则返回null
return null
}
}else{
//处理箭头函数
return eval(funcString)
}
}
function clone(target, map = new WeakMap()) {
//判断是不是引用类型,不是的话则直接返回
if (!isObject(target)) return target
//记录传入的是数组还是对象
const isArray = Array.isArray(target)
//获取当前数据的详细类型
const type = getType(target)
//创建克隆对象
let cloneTarget
//检查是不是可以继续遍历的类型
if (deepTag.includes(type)) {
//创建对应的数据类型实例赋值给克隆对象
cloneTarget = getInit(target)
}else{
//处理不可继续遍历的类型
return cloneOtherType(target, type)
}
//检查该对象有没有被克隆过
if (map.get(target)) {
//如果被克隆过则直接返回该对象
return target
}
//如果没有的话则需要新建键值对存储起来
map.set(target, cloneTarget)
/**
* 克隆Set数据结构
*/
if (type === setTag) {
target.forEach(value => {
//往克隆对象中添加每一个值,并检查是不是引用数据类型
cloneTarget.add(clone(value))
})
//返回克隆的Set数据
return cloneTarget
}
/**
* 克隆Map数据结构
*/
if (type === mapTag) {
target.forEach((value, key) => {
//往克隆对象中添加每一个值,并检查是不是引用数据类型
cloneTarget.set(key, clone(value))
})
//返回克隆的Map数据
return cloneTarget
}
/**
* 克隆数组和对象
*/
//如果是对象,要为他创建对应的keys集合
const keys = isArray ? undefined : Object.keys(target)
//使用我们的forEach去代替for...in优化性能,将属性拷贝到克隆对象中
forEach(keys || target, (value, key) => {
//如果是对象的话,我们需要指定键为value,不然此处遍历出来的是数字
if (keys) key = value
//对每一个属性值进行检查,是引用类型则进一步递归遍历处理
//并且把当前的map数据结构传递给下一个递归过程
cloneTarget[key] = clone(target[key], map)
})
//返回克隆对象或数组
return cloneTarget
}
\