面试官:请你实现一个深拷贝,那如果是正则/set/函数怎么拷贝?
一、面试官灵魂三连问:
- 你知道哪些拷贝的方法?
- 让你实现一个深拷贝怎么实现?
- 那像正则、Set、Map、函数等如何拷贝?
二、浅拷贝方法
自己创建一个新对象,来接收你要重新复制或引用的对象值。如果对象属性是基本数据类型,复制的就是基本数据类型的值给新对象;如果属性是引用数据类型,复制的就是内存中的地址,如果一个对象改变了这个属性值,那么会影响到另一个对象。
1.Object.assign()
- 不会拷贝对象的继承属性
- 不会拷贝对象的不可枚举属性
- 可以拷贝symbol类型
扩展:什么是对象的不可枚举属性?
对象的每一个属性都有一个描述对象,用来描述和控制该对象的属性行为 使用Object.getOwnPropertyDescriptor() 方法来获取描述对象 通过Object.definedProperty() 来设置-----是不是很熟悉,这是vue中数据绑定的方法
例如:
let obj = {
name : '123'
}
console.log(Object.getOwnPropertyDescriptor(obj,'name'))
/* 输出内容:
configurable:true 能否通过delete删除此属性
enumerable : true 表示属性是可以枚举 即是否通过for in 或 Object.keys() 返回属性
value: '123'
writable: true 表示能否修改属性的值
*/
// 设置属性
Object.defineProperty(obj,'名字',{
value:'不可枚举属性',
enumerable:false
})
扩展:如何知道是“不可枚举属性”?
// 方法一:看颜色
console.log(obj)
/*
输出:
{
name: "123" // 控制台里是深色字体
名字: "不可枚举属性" // 控制台里是浅色字体
}
*/
// 方法二:使用以下四个方法
/*
四个操作会忽略enumerable为false
for in
Object.keys()
Object.assign()
JSON.stringify()
*/
// 我们实测下
for(let prop in obj){
console.log(prop)
}
/*
输出:name
*/
console.log(Object.keys(obj))
/*
输出: ['name']
*/
console.log(Object.assign({},obj))
/*
输出: {name: '123'}
*/
console.log(JSON.stringify(obj))
/*
输出: '{"name":"123"}'
*/
2.扩展运算符方式
let obj2 = {...obj1}
let arr2 = [...arr1] //跟arr.slice()一样的效果
3.concat拷贝
let arr1 = [1,2,3]
let arr2 = arr1.concat()
4.slice拷贝数组
let arr1 = [1,2,3]
let arr2 = arr1.slice()
手动实现浅拷贝
var deepClone = target => {
//判断是否是对象类型,不是对象类型的话,直接返回本身
if ((typeof target === "object" || typeof target === 'function') && target !== null) {
//判断目标是数组还是对象
const cloneTarget = Array.isArray(target) ? [] : {};
for (let prop in target) {
//只拷贝自身属性,不拷贝继承属性,所以使用hasOwnProperty(),当属性是继承属性则返回false
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = target[prop];
}
}
return cloneTarget;
} else {
return target;
}
};
三、深拷贝实现方法
1. JSON.stringify() 实现深拷贝
JSON.parse(JSON.stringify(target))
缺点:
- 拷贝的对象中如果存在undefined,function,symbol这几种类型,经过JSON.stringify()序列化后的字符串的这几个键会消失。
- 拷贝Date引用类型会变成字符串
- 无法拷贝不可枚举属性
- 无法拷贝对象的原型链
- 拷贝RexExp引用类型会变成空对象
- 含有NaN,Infinity,-Infinity 经过JSON序列化后会变成null
- 无法拷贝对象循环引用,记对象成环。
2.递归实现深拷贝
function deepCopy(target) {
if((typeof target !== 'object' || typeof target !== 'function') && target === null) return false
let res = Array.isArray(target) ? [] : {}
for(let k in target) {
// 如果目标数据上有属性(键)(key)
if(target.hasOwnProperty(k)) {
// 如果目标数据上属性的值,为object,就递归,不是object,就取到属性值,并放入我们新建的空数组/对象中
res[k] = typeof target[k] === 'object' ? deepCopy(target[k]) : target[k]
}
}
return res
}
测试下:
// test用例1: null
var a = null // false
// test用例2: 不可枚举属性
var b = {name:'张三',like:['girl','cat']}
Object.defineProperty(b,'age',{
value: '18不可枚举属性',
enumerable: false
})
for(let prop in b){
console.log(prop)
}
/*
'name'
'like'
*/
// test用例3: 非数组、对象的引用类型
var c = new Date()
var d = deepCopy(c)
console.log(d)
/*
{}
*/
// test用例4:对象成环
const e = {}
e.e = e
deepClone(e)
/*
出现内存泄漏
*/
// test用例5: 对象、数组深层嵌套
var i = {rep:'apple',like:['women',{gay:{name:'王五',like:[18,'man']}}]}
// 可以
通过测试可以看出
缺点:
- 不可复制不可枚举属性以及symbol类型
- 只针对普通的引用类型做递归
- 没有解决对象成环
3.使用WeakMap解决对象成环
const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && typeof target !== null
function deepClone(target,map = new WeakMap()) {
//当weakmap 中已经存在这个对象,直接返回即可,不用在进行拷贝
if(map.get(target)) return target
if(!isObject(target)) return target
// 没有存在 就在weakmap中添加
map.set(target,true)
let res = Array.isArray(target) ? [] : {}
for(let k in target) {
if(target.hasOwnProperty(k)) {
res[k] = typeof target[k] === 'object' ? deepClone(target[k],map) : target[k]
}
}
return res
}
使用weakmap的原因是 weakmap并不会给对象增加引用次数,即对象可以被垃圾回收机制清除,不会占据内存,造成浪费性能。
四、对非一般引用类型的拷贝方法
由于其他类型较多,所以我们可以将这些类型分类后拷贝,首先看一下lodash 枚举出的一些类型。
按照类型分为可遍历类型和不可遍历类型。可遍历类型比如Set和Map等,不可遍历类型为RegExp和Date等。
扩展:获取数据类型
- typeof:能准确判断基本数据类型,但一般复杂数据类型无法判断
- instanceof:能准确判断复杂数据类型,但基本数据类型不行
- Object.property.toString.call:全部可以
现在,假定我们封装好了isObject,isSet,isDate,等
1.拷贝Set
function deepClone (val) {
if (isSet(val)) {
const set = new Set()
val.forEach(item => {
set.add(deepClone(item))
})
}
}
Map类型和Set类型类似,所以不再重复说明。
2.拷贝正则和Date
function deepClone (val) {
const Ctor = val.constructor
if (isDate(val)) {
return new Ctor(+val)
} else if (isRegExp(val)) {
const reFlags = /\w*$/;
// 此处不用flags的原因在于flags方法返回的修饰符是按照字母顺序排列的
const reg = new Ctor(val.source, reFlags.exec(val))
reg.lastIndex = val.lastIndex
return reg
}
}
其他不可遍历类型类似,每个不可遍历类型有自己的特性,抓住数据类型的特性进行克隆就行。
3.拷贝函数
lodash对函数的处理为直接返回,这点根据函数的特性也能理解,克隆函数实际并无意义。如果必须实现的话需要考虑箭头函数和普通函数,箭头函数是没有原型的。克隆箭头函数比较简单,直接调用函数的toString方法,然后eval解析即可,普通函数需要正则匹配函数体,再通过new Function生成。
五、答读者问
有读者很仔细,动手能力不错,作者表示鼓励!
读者问: 为什么采用了三-3的方式,还是没有解决引用成环?
作者答: 你能打印出结果来,就说明已经解决了。
但作者仔细一想,提出这个问题,说明缺乏些前置知识,我这么回答,不懂的读者还是会不懂。因此,作者再补充些前置知识。
1.什么是“引用成环”?
拷贝对象时,当对象存在循环引用,即对象的属性间接或直接的引用了自身的情况,就会出现 引用成环问题,即出现对象无限嵌套循环,会导致“最大数量超过栈大小”,即内存泄漏。
2.引用成环带来的问题
此时浏览器会出现报错提示:“Maximum call stack size exceeded”。
如图:
即:三-2 的 test用例4
3.解决引用成环带来的问题的方法
成熟的做法是,在clone的过程中,记住每个已经clone的对象属性,并且在对对象进行深度clone之前,首先检查是否已经clone过了,如果是,则返回已clone的引用。
经改造后,我们能成功拷贝引用成环对象,没有出现内存泄漏~
即:三-3 方法
4.什么是WeakMap?
定义:
WeakMap对象保存键值对,与Map不同的是其键必须是对象,因为键是弱引用,在键对象消失后自动释放内存。
语法:
new WeakMap([iterable])
方法:
- set(key, value):设置(新增/更新)键key的值为value,返回WeakMap对象。
- get(key):读取键key的值,没有则返回undefined。
- has(key):判断一个WeakMap对象中是否存在某个键值对,返回true/false。
- delete(key):删除某个键值对,返回true/false。
注意
因为WeakMap的特殊的垃圾回收机制,所以没有clear()方法。
5.为什么使用WeakMap?
据 五-3 所述,“记住每个已经clone的对象属性”,那我们就需要一个容器来“记住”。当我们不再需要这个容器,就必须手动消除这个容器,否则垃圾回收机制就不会释放这个容器的占用。
据 五-4 所述,我们可以看出,WeakMap有方便我们存取的方法,同时不需要时也会自动释放内存。
因此,WeakMap当这个容器自然是不二之选!
转载自:https://juejin.cn/post/7136723425199915016