<script>
// 深拷貝實(shí)現(xiàn)流程
// 基礎(chǔ)版
// 如果是前拷貝取胎,我們可以很容易寫出下面的代碼
function clone(target){
let obj = {}
for(const i in target){
obj[i] = target[i]
}
return obj
}
// 如果是深拷貝的話,考慮到我們要拷貝的對象是不知道有多少層深度的辕坝,我們可以用遞歸解決問題誊册,稍微改寫上面的代碼:
// 领突。如果是原始類型,無需繼續(xù)拷貝案怯,直接返回
// 君旦。如果是引用類型,創(chuàng)建一個新的對象嘲碱,遍歷需要克隆的對象金砍,將需要克隆對象的屬性執(zhí)行深拷貝后依次添加到新對象上
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');
// 這是一個最基礎(chǔ)版本的深拷貝,這段代碼可以讓你向面試官展示你可以用遞歸解決問題麦锯,
// 但是還有很多缺陷恕稠,比如數(shù)組
// 只需要稍微改動一下,就可以兼容數(shù)組了
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 循環(huán)引用 遞歸進(jìn)入死循環(huán)導(dǎo)致棧內(nèi)存溢出
// console.log(clone3(target2), 'target4444') // 循環(huán)引用報錯
// 循環(huán)引用
// 解決循環(huán)引用問題扶欣,我們可以額外開辟一個存儲空間鹅巍,來存儲當(dāng)前對象和拷貝對象的對應(yīng)關(guān)系千扶,當(dāng)需要拷貝當(dāng)前對象時,先去存儲空間中去找骆捧,
// 如果有的話直接返回澎羞,如果沒有的話繼續(xù)拷貝
// 這樣就巧妙化解循環(huán)陰影的問題
// 這個存儲空間,需要可以存儲key-value形式的數(shù)據(jù)敛苇,且key可以是一個引用類型煤痕,我們可以選擇Map這種數(shù)據(jù)結(jié)構(gòu):
// 。檢查map中有無克隆過對象
// 接谨。有-直接返回
// 摆碉。沒有-將當(dāng)前對象作為key,克隆對象作為value進(jìn)行存儲
// 脓豪。繼續(xù)克隆
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適合用來遍歷具有迭代器的數(shù)組/字符串/map/set等集合
// for(const [key,vale] of map) 這種解構(gòu)循環(huán)適合用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)]
// 執(zhí)行沒有報錯巷帝,且self屬性的指向正確
// 接下來,可以使用扫夜,WeakMap替代Map來使代碼達(dá)到化龍點(diǎn)睛的作用
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對象是一組鍵值對楞泼,其中的鍵是弱引用的。其中的鍵必須是對象笤闯,而值可以是任意的
// 什么是弱引用的呢
// 在計算機(jī)中堕阔,弱引用和強(qiáng)引用相對,是指不能確保其引用的對象不會被垃圾回收器回收的引用颗味。
// 一個對象若是只被弱用用引用超陆,則被認(rèn)為是不可訪問(或弱可訪問)的,并因此可能在任何時刻被回收
// 我們默認(rèn)創(chuàng)建一個對象:const obj={},就默認(rèn)創(chuàng)建了一個強(qiáng)引用的對象浦马,我們只有手動將obj = null时呀,
// 它才會被垃圾回收機(jī)制進(jìn)行回收
// 如果是弱引用對象,垃圾回收機(jī)制會自動幫我們回收
// 舉個例子
// 如果我們使用Map的話晶默,那么對象間存在強(qiáng)引用關(guān)系的
let obj = {name: 'ConardLi'}
const themap = new Map()
themap.set(obj, 'code秘密花園')
// obj = null
// 雖然我們手動將obj進(jìn)行釋放谨娜,但是themap依然對obj存在強(qiáng)引用關(guān)系,所以這部分內(nèi)存依然無法被釋放
// 假設(shè)使用的是WeakMap
const theweapmap = new WeakMap()
theweapmap.set(obj,'code秘密花園')
// 如果是WeakMap的話磺陡,theweapmap和obj存在的就是弱引用關(guān)系趴梢,當(dāng)下一次垃圾回放機(jī)制執(zhí)行時,這塊內(nèi)存就會被釋放掉
// 設(shè)想一下币他,如果我們要拷貝的對象非常龐大時坞靶,使用Map會對內(nèi)存造成非常大的消耗,而且我們需要手動清除Map的屬性才能釋放這塊內(nèi)存圆丹,
// 而WeakMap會幫我們巧妙化解這個問題
// 性能優(yōu)化
// 在上面的代碼中滩愁,我們遍歷數(shù)組和對象都使用了for in這種方式,實(shí)際上for in在遍歷時效率是非常低的辫封,
// 我們來對比下常見的三種循環(huán)for硝枉,while,for in的執(zhí)行效率
// while 4s for 12 for in 141s
// 可以看到倦微,while的效率是最好的妻味,所以我們可以想辦法把for in遍歷改變?yōu)閣hile遍歷
// 我們可以先使用while來實(shí)現(xiàn)一個通用的forEach遍歷,iteratee是遍歷的回調(diào)函數(shù)欣福,他可以接收每次遍歷的value和index兩個參數(shù)
// iteratee 是遍歷的回調(diào)函數(shù)
function foreach(array, iteratee){
let index = -1
const length = array.length
while (++index < length){
iteratee(array[index], index)
}
return array
}
// 下面對我們的clone函數(shù)進(jìn)行改寫:當(dāng)遍歷數(shù)組的時候责球,直接進(jìn)行forEach遍歷,當(dāng)遍歷對象時拓劝,使用Object.keys取出所有的key進(jìn)行遍歷雏逾,
// 然后在遍歷時調(diào)函數(shù)的value當(dāng)key使用
function clone6(target,map=new WeakMap()){
if(typeof target === [Object]){
const isArray = Array.isArray(target)
let cloneTarget = isArray ? [] : {}
// 將target作為map的鍵值,存儲對應(yīng)的cloneTarget郑临,
// 在每次遞歸的時候栖博,判斷是否有對應(yīng)的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');
// 其他數(shù)據(jù)結(jié)構(gòu)
// 在上面的代碼中,我們其實(shí)只考慮了普通的object和array兩種數(shù)據(jù)類型厢洞,
// 實(shí)際上所有的引用類型遠(yuǎn)遠(yuǎn)不止這兩個仇让,下面我們先嘗試獲取對象的準(zhǔn)確類型
// 合理的類型判斷
// 引用類型指那些可能由多個值構(gòu)成的對象
// 5種基本類型包括:Undefined,Null躺翻,Boolean丧叽,Number和String
// 引用類型的值是保存在內(nèi)存中的對象。
// 與其他語言不同的是公你,Javascript不允許直接訪問內(nèi)存中的位置踊淳,也就是說不能直接操作對象的內(nèi)存空間
// 在操作對象時,實(shí)際上是在操作對象的引用而不是實(shí)際的對象
// 首先陕靠,判斷是否為引用類型嚣崭,我們還需要考慮function和null兩種特殊的數(shù)據(jù)類型
function isObject(target){
const type = typeof target
return target !== null&(type === 'object' || type === 'function')
}
// if(!isObject(target)){
// return target
// }
// 獲取數(shù)據(jù)類型
// 我們可以使用toString來獲取準(zhǔn)確的引用類型
// 每一個數(shù)據(jù)都有toString方法,默認(rèn)情況下懦傍,toString方法被每個Object對象繼承雹舀。
// 如果此方法在自定義對象中未被覆蓋,toString返回'[object type]',其中type是對象的類型
// 注意粗俱,上面提到了如果此方法在自定義對象中未被覆蓋说榆,toString才會達(dá)到預(yù)想的效果,事實(shí)上寸认,大部分引用類型比如Array签财,Date,RegExp等都重寫了
// toString方法
// 我們可以直接調(diào)用Object原型上未被覆蓋的toString方法偏塞,使用call來改變this指向來達(dá)到我們想要的效果
function getType(target){
return Object.prototype.toString.call(target)
}
// 下面我們可以抽離出一些常用的數(shù)據(jù)類型以便后面使用
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]
// 上面的集中類型中唱蒸,我們簡單將他們分為兩類
// 【牡穑可以繼續(xù)遍歷的對象
// 神汹。不可以繼續(xù)遍歷的對象
// 我們分別作不同的拷貝
// 可繼續(xù)遍歷的類型
// 上面我們已經(jīng)考慮的object庆捺,array都屬于可以繼續(xù)遍歷的對象,因?yàn)樗麄儍?nèi)存都還可以存儲其他數(shù)據(jù)類型的數(shù)據(jù)屁魏,
// 另外滔以,還有Map,Set等都是可以繼續(xù)遍歷的類型氓拼,這里我們只考慮這四種你画,如果你有興趣可以繼續(xù)探索其他類型
// 有序這幾種類型還需要繼續(xù)進(jìn)行遞歸,我們首先需要獲取它們的初始化數(shù)據(jù)桃漾,例如上面的[]和{},
// 我們可以通過拿到constructor的方式來通用的獲取
// 例如:const target={} 就是const target= new Object()的語法糖坏匪。
// 另外,這種方法還有一個好處:因?yàn)槲覀兪褂昧嗽瓕ο蟮臉?gòu)造方法
// 所以它可以保留對象原型上的數(shù)據(jù)
// 如果直接使用普通的{},那么原型必然是丟失的
function getInit(target){
const Ctor = target.constructor
return new Ctor()
}
// 下面撬统,我們改寫clone函數(shù)适滓,對可繼續(xù)遍歷的數(shù)據(jù)類型進(jìn)行處理
function clone7(target, map=new WeakMap()){
// 克隆原始類型
if(!isObject(target)){
return target
}
// 初始化
const type = getType(target)
let cloneTarget
if(deepTag.includes(type)){
cloneTarget = getInit(target)
}
// 防止循環(huán)引用
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
}
// 克隆對象和數(shù)組
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
}
// 執(zhí)行下面的用例進(jìn)行測試
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') // 復(fù)制成功
// 下面繼續(xù)處理其他類型
// 不可繼續(xù)遍歷的類型
// 其他剩余的類型我們把它們統(tǒng)一歸類成不可處理的數(shù)據(jù)類型,我們依次處理
// 這幾種類型我們都可以直接用構(gòu)造函數(shù)和原始數(shù)據(jù)創(chuàng)建一個新對象
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
}
// 寫到這里宪摧,面試官已經(jīng)看到了你考慮問題的嚴(yán)謹(jǐn)性粒竖,你對變量和類型的理解,
// 對JS API的熟練程度
// 克隆函數(shù)
// 實(shí)際上克隆函數(shù)是沒有實(shí)際應(yīng)用場景的几于,兩個對象使用一個在內(nèi)存中處于同一個地址的函數(shù)是沒有任何問題的
// const isFunc = typeof value == 'function'
// if(isFunc || !cloneableTags[tag]){
// return object ? value : {}
// }
</script>