<script>
// 深拷贝实现流程
// 基础版
// 如果是前拷贝,我们可以很容易写出下面的代码
function clone(target){
let obj = {}
for(const i in target){
obj[i] = target[i]
}
return obj
}
// 如果是深拷贝的话,考虑到我们要拷贝的对象是不知道有多少层深度的,我们可以用递归解决问题,稍微改写上面的代码:
// 。如果是原始类型,无需继续拷贝,直接返回
// 。如果是引用类型,创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性执行深拷贝后依次添加到新对象上
function clone2(target){
if(typeof target === 'object'){
let cloneTarget = {}
for(const key in target){
console.log('target', key, target[key])
cloneTarget[key] = clone2(target[key])
}
return cloneTarget
}else{
console.log(target, 'finallytarget')
return target
}
}
const target = [
1,2,{
child:3,
child2:{
child3:{
child4:4
}
}
}
]
console.log(clone2(target), 'target');
// 这是一个最基础版本的深拷贝,这段代码可以让你向面试官展示你可以用递归解决问题,
// 但是还有很多缺陷,比如数组
// 只需要稍微改动一下,就可以兼容数组了
function clone3(target){
if(typeof target === 'object'){
let cloneTarget = Array.isArray(target) ? [] : {}
for(const key in target){
cloneTarget[key] = clone3(target[key])
}
return cloneTarget
}else{
return target
}
}
const target2 = [
[1,[2,3]],
{
parent:{
child:{
child2:{
child3:4
}
}
}
}
]
target2.self = target2 // Maximum call stack size exceeded 循环引用 递归进入死循环导致栈内存溢出
// console.log(clone3(target2), 'target4444') // 循环引用报错
// 循环引用
// 解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中去找,
// 如果有的话直接返回,如果没有的话继续拷贝
// 这样就巧妙化解循环阴影的问题
// 这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构:
// 。检查map中有无克隆过对象
// 。有-直接返回
// 。没有-将当前对象作为key,克隆对象作为value进行存储
// 。继续克隆
function clone4(target,map = new Map()){
if(typeof target === 'object'){
let cloneTarget = Array.isArray(target) ? [] : {}
if(map.get(target)){
return map.get(target)
}
map.set(target, cloneTarget)
// console.log(map, 'map11111111')
// for in适合遍历对象 for of适合用来遍历具有迭代器的数组/字符串/map/set等集合
// for(const [key,vale] of map) 这种解构循环适合用map
for(const key in target){
cloneTarget[key] = clone4(target[key],map)
}
return cloneTarget
}else{
return target
}
}
console.log(clone4(target2), 'clone4') // 0: (2) [1, Array(2)] 1: {parent: {…}} self: (2) [Array(2), {…}, self: Array(2)]
// 执行没有报错,且self属性的指向正确
// 接下来,可以使用,WeakMap替代Map来使代码达到化龙点睛的作用
function clone5(target,map = new WeakMap()){
if(typeof target === 'object'){
let cloneTarget = Array.isArray(target) ? [] : {}
if(map.get(target)){
return map.get(target)
}
map.set(target, cloneTarget)
for(let key in target){
cloneTarget[key] = clone5(target[key], map)
}
return cloneTarget
}else{
return target
}
}
console.log(clone5(target2),'clone5')
// WeakMap的作用
// WeakMap对象是一组键值对,其中的键是弱引用的。其中的键必须是对象,而值可以是任意的
// 什么是弱引用的呢
// 在计算机中,弱引用和强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。
// 一个对象若是只被弱用用引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收
// 我们默认创建一个对象:const obj={},就默认创建了一个强引用的对象,我们只有手动将obj = null,
// 它才会被垃圾回收机制进行回收
// 如果是弱引用对象,垃圾回收机制会自动帮我们回收
// 举个例子
// 如果我们使用Map的话,那么对象间存在强引用关系的
let obj = {name: 'ConardLi'}
const themap = new Map()
themap.set(obj, 'code秘密花园')
// obj = null
// 虽然我们手动将obj进行释放,但是themap依然对obj存在强引用关系,所以这部分内存依然无法被释放
// 假设使用的是WeakMap
const theweapmap = new WeakMap()
theweapmap.set(obj,'code秘密花园')
// 如果是WeakMap的话,theweapmap和obj存在的就是弱引用关系,当下一次垃圾回放机制执行时,这块内存就会被释放掉
// 设想一下,如果我们要拷贝的对象非常庞大时,使用Map会对内存造成非常大的消耗,而且我们需要手动清除Map的属性才能释放这块内存,
// 而WeakMap会帮我们巧妙化解这个问题
// 性能优化
// 在上面的代码中,我们遍历数组和对象都使用了for in这种方式,实际上for in在遍历时效率是非常低的,
// 我们来对比下常见的三种循环for,while,for in的执行效率
// while 4s for 12 for in 141s
// 可以看到,while的效率是最好的,所以我们可以想办法把for in遍历改变为while遍历
// 我们可以先使用while来实现一个通用的forEach遍历,iteratee是遍历的回调函数,他可以接收每次遍历的value和index两个参数
// iteratee 是遍历的回调函数
function foreach(array, iteratee){
let index = -1
const length = array.length
while (++index < length){
iteratee(array[index], index)
}
return array
}
// 下面对我们的clone函数进行改写:当遍历数组的时候,直接进行forEach遍历,当遍历对象时,使用Object.keys取出所有的key进行遍历,
// 然后在遍历时调函数的value当key使用
function clone6(target,map=new WeakMap()){
if(typeof target === [Object]){
const isArray = Array.isArray(target)
let cloneTarget = isArray ? [] : {}
// 将target作为map的键值,存储对应的cloneTarget,
// 在每次递归的时候,判断是否有对应的target键值
if(map.get(target)){
return map.get(target)
}
map.set(target, cloneTarget)
const keys = isArray ? null : Object.keys(target)
foreach(keys||target,(value,key) => {
if(keys){
key = value
}
cloneTarget[key] = clone2(target[key],map)
})
return cloneTarget
}else{
return target
}
}
console.log(clone6(target2), 'target6');
// 其他数据结构
// 在上面的代码中,我们其实只考虑了普通的object和array两种数据类型,
// 实际上所有的引用类型远远不止这两个,下面我们先尝试获取对象的准确类型
// 合理的类型判断
// 引用类型指那些可能由多个值构成的对象
// 5种基本类型包括:Undefined,Null,Boolean,Number和String
// 引用类型的值是保存在内存中的对象。
// 与其他语言不同的是,Javascript不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间
// 在操作对象时,实际上是在操作对象的引用而不是实际的对象
// 首先,判断是否为引用类型,我们还需要考虑function和null两种特殊的数据类型
function isObject(target){
const type = typeof target
return target !== null&(type === 'object' || type === 'function')
}
// if(!isObject(target)){
// return target
// }
// 获取数据类型
// 我们可以使用toString来获取准确的引用类型
// 每一个数据都有toString方法,默认情况下,toString方法被每个Object对象继承。
// 如果此方法在自定义对象中未被覆盖,toString返回'[object type]',其中type是对象的类型
// 注意,上面提到了如果此方法在自定义对象中未被覆盖,toString才会达到预想的效果,事实上,大部分引用类型比如Array,Date,RegExp等都重写了
// toString方法
// 我们可以直接调用Object原型上未被覆盖的toString方法,使用call来改变this指向来达到我们想要的效果
function getType(target){
return Object.prototype.toString.call(target)
}
// 下面我们可以抽离出一些常用的数据类型以便后面使用
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 regexpTag = '[object RegExp]'
const stringTag = '[object String]'
const symbolTag = '[object Symbol]'
let deepTag = [mapTag,setTag,arrayTag,objectTag, boolTag,dateTag,errorTag,numberTag,regepTag,stringTag,symbolTag]
// 上面的集中类型中,我们简单将他们分为两类
// 。可以继续遍历的对象
// 。不可以继续遍历的对象
// 我们分别作不同的拷贝
// 可继续遍历的类型
// 上面我们已经考虑的object,array都属于可以继续遍历的对象,因为他们内存都还可以存储其他数据类型的数据,
// 另外,还有Map,Set等都是可以继续遍历的类型,这里我们只考虑这四种,如果你有兴趣可以继续探索其他类型
// 有序这几种类型还需要继续进行递归,我们首先需要获取它们的初始化数据,例如上面的[]和{},
// 我们可以通过拿到constructor的方式来通用的获取
// 例如:const target={} 就是const target= new Object()的语法糖。
// 另外,这种方法还有一个好处:因为我们使用了原对象的构造方法
// 所以它可以保留对象原型上的数据
// 如果直接使用普通的{},那么原型必然是丢失的
function getInit(target){
const Ctor = target.constructor
return new Ctor()
}
// 下面,我们改写clone函数,对可继续遍历的数据类型进行处理
function clone7(target, map=new WeakMap()){
// 克隆原始类型
if(!isObject(target)){
return target
}
// 初始化
const type = getType(target)
let cloneTarget
if(deepTag.includes(type)){
cloneTarget = getInit(target)
}
// 防止循环引用
if(map.get(target)){
return map.get(target)
}
map.set(target, cloneTarget)
// 克隆Set
if(type === setTag){
target.forEach(value => {
cloneTarget.add(clone7(value,map))
})
return cloneTarget
}
// 克隆map
if(type === mapTag){
target.forEach((value,key) => {
cloneTarget.set(key,clone7(value,map))
})
return cloneTarget
}
// 克隆对象和数组
const keys = type === arrayTag ? undefined : Object.keys(target)
foreach(keys || target,(value, key) => {
if(keys){
key = value
}
cloneTarget[key] = clone7(target[key], map)
})
return target
}
// 执行下面的用例进行测试
let map = new Map([['a',1],['b',2],[3,'c']])
let set = new Set([1,2,3,6])
const target4 = {
field1:1,
field2:undefined,
field3:{
child:'child'
},
field4:[2,4,8],
empty:null,
map,
set
}
console.log(clone7(target4), 'clone7') // 复制成功
// 下面继续处理其他类型
// 不可继续遍历的类型
// 其他剩余的类型我们把它们统一归类成不可处理的数据类型,我们依次处理
// 这几种类型我们都可以直接用构造函数和原始数据创建一个新对象
function cloneOtherObject(){
const Ctor = target.constructor
switch(type){
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(target)
case regexpTag:
return cloneReg(target)
case symbolTag:
return cloneSymbol(target)
default:
return null
}
}
// 克隆Symbol类型
function cloneSymbol(target){
return Object(Symbol.prototype.valueOf.call(target))
}
//克隆正则
function cloneReg(target){
const reFlags = /\w*$/
const result = new target.constructor(target.source, reFlags.exec(target))
result.lastIndex = target.lastIndex
return result
}
// 写到这里,面试官已经看到了你考虑问题的严谨性,你对变量和类型的理解,
// 对JS API的熟练程度
// 克隆函数
// 实际上克隆函数是没有实际应用场景的,两个对象使用一个在内存中处于同一个地址的函数是没有任何问题的
// const isFunc = typeof value == 'function'
// if(isFunc || !cloneableTags[tag]){
// return object ? value : {}
// }
</script>
网友评论