欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

前端面試的底氣之實現(xiàn)一個深拷貝

 更新時間:2022年05月10日 14:25:56   作者:紅塵煉心  
最近學到一個有關深拷貝的實現(xiàn)方法,為加深印象,這里給大家分享一下,下面這篇文章主要給大家介紹了關于前端面試的底氣之實現(xiàn)一個深拷貝的相關資料,需要的朋友可以參考下

前言

深拷貝這個功能在開發(fā)中經(jīng)常使用到,特別在對引用類型的數(shù)據(jù)進行操作時,一般會先深拷貝一份賦值給一個變量,然后在對其操作,防止影響到其它使用該數(shù)據(jù)的地方。

如何實現(xiàn)一個深拷貝,在面試中出現(xiàn)頻率一直居高不下。因為在實現(xiàn)一個深拷貝過程中,可以看出應聘者很多方面的能力。

本專欄將從青銅到王者來介紹怎么實現(xiàn)一個深拷貝,以及每個段位對應的能力。

青銅段位

JSON.parse(JSON.stringify(data))

這種寫法非常簡單,而且可以應對大部分的應用場景,但是它有很大缺陷的。如果你不知道它有那些缺陷,而且這種實現(xiàn)方法體現(xiàn)不出你任何能力,所以這種實現(xiàn)方法處于青銅段位。

  • 如果對象中存在循環(huán)引用的情況也無法正確實現(xiàn)深拷貝。
const a = {
    b: 1,
}
a.c = a;
JSON.parse(JSON.stringify(a));

  • 如果 data 里面有時間對象,則JSON.stringify后再JSON.parse的結果,時間將只是字符串的形式。而不是時間對象。
const a = {
    b: new Date(1536627600000),
}
console.log(JSON.parse(JSON.stringify(a)))

  • 如果 data 里有RegExp、Error對象,則序列化的結果將只得到空對象;
const a = {
    b: new RegExp(/\d/),
    c: new Error('錯誤')
}
console.log(JSON.parse(JSON.stringify(a)))

  • 如果 data 里有函數(shù),undefined,則序列化的結果會把函數(shù)置為undefined或丟失;
const a = {
    b: function (){
        console.log(1)
    },
    c:1,
	d:undefined
}
console.log(JSON.parse(JSON.stringify(a)))

  • 如果 data 里有NaN、Infinity和-Infinity,則序列化的結果會變成null
const a = {
    b: NaN,
    c: 1.7976931348623157E+10308,
    d: -1.7976931348623157E+10308,
}
console.log(JSON.parse(JSON.stringify(a)))

白銀段位

深拷貝的核心就是對引用類型的數(shù)據(jù)的拷貝處理。

function deepClone(target){
    if(target !== null && typeof target === 'object'){
        let result = {}
        for (let k in target){
            if (target.hasOwnProperty(k)) {
                result[k] = deepClone(target[k])
            }
        }
        return result;
    }else{
        return target;
    }
}

以上代碼中,deepClone函數(shù)的參數(shù) target 是要深拷貝的數(shù)據(jù)。

執(zhí)行 target !== null && typeof target === 'object' 判斷 target 是不是引用類型。

若不是,直接返回 target。

若是,創(chuàng)建一個變量 result 作為深拷貝的結果,遍歷 target,執(zhí)行 deepClone(target[k]) 把 target 每個屬性的值深拷貝后賦值到深拷貝的結果對應的屬性 result[k] 上,遍歷完畢后返回 result。

在執(zhí)行 deepClone(target[k]) 中,又會對 target[k] 進行類型判斷,重復上述流程,形成了一個遞歸調(diào)用 deepClone 函數(shù)的過程。就可以層層遍歷要拷貝的數(shù)據(jù),不管要拷貝的數(shù)據(jù)有多少子屬性,只要子屬性的值的類型是引用類型,就會調(diào)用 deepClone 函數(shù)將其深拷貝后賦值到深拷貝的結果對應的屬性上。

另外使用 for...in 循環(huán)遍歷對象的屬性時,其原型鏈上的所有屬性都將被訪問,如果只要只遍歷對象自身的屬性,而不遍歷繼承于原型鏈上的屬性,要使用 hasOwnProperty 方法過濾一下。

在這里可以向面試官展示你的三個編程能力。

  • 對原始類型和引用類型數(shù)據(jù)的判斷能力。
  • 對遞歸思維的應用的能力。
  • 深入理解for...in的用法。

黃金段位

白銀段位的代碼中只考慮到了引用類型的數(shù)據(jù)是對象的情況,漏了對引用類型的數(shù)據(jù)是數(shù)組的情況。

function deepClone(target){
    if(target !== null && typeof target === 'object'){
        let result = Object.prototype.toString.call(target) === "[object Array]" ? [] : {};
        for (let k in target){
            if (target.hasOwnProperty(k)) {
                result[k] = deepClone(target[k])
            }
        }
        return result;
    }else{
        return target;
    }
}

以上代碼中,只是額外增加對參數(shù) target 是否是數(shù)組的判斷。執(zhí)行 Object.prototype.toString.call(target) === "[object Array]" 判斷 target 是不是數(shù)組,若是數(shù)組,變量result 為 [],若不是數(shù)組,變量result 為 {}。

在這里可以向面試官展示你的兩個編程能力。

  • 正確理解引用類型概念的能力。
  • 精確判斷數(shù)據(jù)類型的能力。

鉑金段位

假設要深拷貝以下數(shù)據(jù) data

let data = {
    a: 1
};
data.f=data

執(zhí)行 deepClone(data),會發(fā)現(xiàn)控制臺報錯,錯誤信息如下所示。

這是因為遞歸進入死循環(huán)導致棧內(nèi)存溢出了。根本原因是 data 數(shù)據(jù)存在循環(huán)引用,即對象的屬性間接或直接的引用了自身。

function deepClone(target) {
    function clone(target, map) {
        if (target !== null && typeof target === 'object') {
            let result = Object.prototype.toString.call(target) === "[object Array]" ? [] : {};
            if (map[target]) {
                return map[target];
            }
            map[target] = result;
            for (let k in target) {
                if (target.hasOwnProperty(k)) {
                    result[k] = clone(target[k],map)
                }
            }
            return result;
        } else {
            return target;
        }
    }
    let map = new Map();
    const result = clone(target, map);
    map.clear();
    map = null;
    return result
}

以上代碼中利用額外的變量 map 來存儲當前對象和拷貝對象的對應關系,當需要拷貝當前對象時,先去 map 中找,有沒有拷貝過這個對象,如果有的話直接返回,如果沒有的話繼續(xù)拷貝,這樣就巧妙化解的循環(huán)引用的問題。

之所以采用 ES6 中 Map 數(shù)據(jù)結構,是因為相對于普通的 Object 結構,其鍵值不限于字符串,各種類型的值(包括對象)都可以當作鍵。換句話來說。Object 結構提供了“字符串——值”的對應,Map 結構提供了“值——值” 的對應。若采用 Object 結構,當 target 不是字符串時,那么其鍵值全部都是 [Object Object],會引起混亂。

最后需要執(zhí)行 map.clear();map = null; ,釋放內(nèi)存,防止內(nèi)存泄露。

在這里可以向面試官展示你的三個編程能力。

  • 對循環(huán)引用的理解,如何解決循環(huán)引用引起的問題的能力。
  • 熟悉 ES6 中 Map 數(shù)據(jù)結構的概念及應用
  • 對內(nèi)存泄露的認識和避免泄露的能力。

磚石段位

該段位要考慮性能問題了。上面用 Map 來解決循環(huán)引用的問題。但是最后一定要執(zhí)行 map.clear();map = null; ,釋放內(nèi)存,防止內(nèi)存泄露。

也可以使用 WeakMap 來解決循環(huán)引用。WeakMap 數(shù)據(jù)結構和 Map 數(shù)據(jù)結構,有兩點區(qū)別:

  • WeakMap 只接受對象作為鍵名( null 除外),不接受其他類型的值作為鍵名。但是 target 正好符合要求故不影響。

  • WeakMap 的鍵名所指向的對象不計入垃圾回收機制。這里用下面的例子來解釋。

let map = new WeakMap();
let obj = {a : 1}
map.set(obj , 1);

現(xiàn)代瀏覽器GC回收策略,采用計算引用次來回收,就是某個對象被引用的次數(shù)為 1,因為 map 中的鍵名 obj 和對象 obj 之間的引用是弱引用,也就是不計入引用次數(shù),對象 obj 被引用的次數(shù)還是 1。 當執(zhí)行 obj = null后,執(zhí)行GC回收時,對象 obj 被引用的次數(shù)變成 0 ,故會把 obj 所占的內(nèi)存釋放掉。

但是如果 map 是 Map 數(shù)據(jù)結構,因為 Map 的鍵名所引用的對象是強引用,就算執(zhí)行 obj = null 后,再執(zhí)行GC回收,Map 的鍵名 obj 對所引用的對象 obj 還是有引用,那么obj 被引用的次數(shù)還是 1,故 obj 所占的內(nèi)存釋放不掉。

所以使用 WeakMap 可以防止忘記把 map 置為 null 導致的內(nèi)存泄漏。然而 WeakMap 這種弱引用,僅僅只是為了防止內(nèi)存泄漏嗎?其還有個特性,當執(zhí)行 obj = null,相當對象 obj 被GC回收了,但是執(zhí)行 map.get(obj) 得到的值還是 1。我們可以利用這個特性來做一下性能優(yōu)化。

在深拷貝的代碼中。是使用拷貝的對象作為鍵名的,設想一下,當拷貝的對象非常龐大時,導致 map 會占用很大的內(nèi)存。

如果 map 使用 Map 數(shù)據(jù)結構,因為 Map 的鍵名所引用的對象是強引用,所以在瀏覽器定時執(zhí)行GC回收時,除非手動清除 Map 這個鍵值對,才能把這個拷貝對象所占用的內(nèi)存釋放掉。

如果 map 使用 WeakMap 數(shù)據(jù)結構,因為 WeakMap 的鍵名所引用的對象是弱引用,所以在瀏覽器會定時執(zhí)行GC回收,會直接把這個拷貝對象所占用的內(nèi)存釋放掉,那么這樣是不是對內(nèi)存占用減少,間接優(yōu)化了代碼運行性能。

此外在上面的代碼中,我們遍歷數(shù)組和對象都使用了 for...in 這種方式,實際上 for...in 在遍歷時效率是非常低的,故用效率比較高的 while 來遍歷。

function deepClone(target) {
    /**
     * 遍歷數(shù)據(jù)處理函數(shù)
     * @array 要處理的數(shù)據(jù)
     * @callback 回調(diào)函數(shù),接收兩個參數(shù) value 每一項的值 index 每一項的下標或者key。
    */
    function handleWhile(array, callback) {
        const length = array.length;
        let index = -1;
        while (++index < length) {
            callback(array[index], index)
        }
    }
    function clone(target, map) {
        if (target !== null && typeof target === 'object') {
            let result = Object.prototype.toString.call(target) === "[object Array]" ? [] : {};
            // 解決循環(huán)引用
            if (map.has(target)) {
                return map.get(target);
            }
            map.set(target, result);
            
            const keys = Object.prototype.toString.call(target) === "[object Array]" ? undefined : Object.keys(
                target);

            function callback(value, key) {
                if (keys) {
                    // 如果keys存在則說明value是一個對象的key,不存在則說明key就是數(shù)組的下標。
                    key = value;
                }
                result[key] = clone(target[key], map)
            }
            handleWhile(keys || target, callback)
            return result;
        } else {
            return target;
        }
    }
    let map = new WeakMap();
    const result = clone(target,map);
    map = null;
    return result
}

用 while 遍歷的深拷貝記為 deepClone,把用 for ... in 遍歷的深拷貝記為 deepClone1。利用 console.time()console.timeEnd() 來計算執(zhí)行時間。

let arr = [];
for (let i = 0; i < 1000000; i++) {
    arr.push(i)
}
let data = {
    a: arr
};
console.time();
const result = deepClone(data);
console.timeEnd();
console.time();
const result1 = deepClone1(data);
console.timeEnd();

從上圖明顯可以看到用 while 遍歷的深拷貝的性能遠優(yōu)于用 for ... in 遍歷的深拷貝。

在這里可以向面試官展示你的五個編程能力。

  • 熟悉 ES6 中 WeakMap 數(shù)據(jù)結構的概念及應用
  • 具有優(yōu)化代碼運行性能的能力。
  • 了解遍歷的效率的能力。
  • 了解 ++ii++ 的區(qū)別。
  • 代碼抽象的能力。

星耀段位

在這個階段應該考慮代碼邏輯的嚴謹性。在上面段位的代碼雖然已經(jīng)滿足平時開發(fā)的需求,但是還是有幾處邏輯不嚴謹?shù)牡胤健?/p>

  • 判斷數(shù)據(jù)不是引用類型時就直接返回 target,但是原始類型中還有 Symbol 這一特殊類型的數(shù)據(jù),因為其每個 Symbol 都是獨一無二,需要額外拷貝處理,不能直接返回。

  • 判斷數(shù)據(jù)是不是引用類型時不嚴謹,漏了 typeof target === function' 的判斷。

  • 只考慮了 Array、Object 兩種引用類型數(shù)據(jù)的處理,引用類型的數(shù)據(jù)還有Function 函數(shù)、Date 日期、RegExp 正則、Map 數(shù)據(jù)結構、Set 數(shù)據(jù)機構,其中 Map 、Set 屬于 ES6 的。

廢話不多說,直接貼上全部代碼,代碼中有注釋。

function deepClone(target) {
    // 獲取數(shù)據(jù)類型
    function getType(target) {
        return Object.prototype.toString.call(target)
    }
    //判斷數(shù)據(jù)是不是引用類型
    function isObject(target) {
        return target !== null && (typeof target === 'object' || typeof target === 'function');
    }
    //處理不需要遍歷的應引用類型數(shù)據(jù)
    function handleOherData(target) {
        const type = getType(target);
        switch (type) {
            case "[object Date]":
                return new Date(target)
            case "[object RegExp]":
                return cloneReg(target)
            case "[object Function]":
                return cloneFunction(target)

        }
    }
    //拷貝Symbol類型數(shù)據(jù)
    function cloneSymbol(targe) {
        const a = String(targe); //把Symbol字符串化
        const b = a.substring(7, a.length - 1); //取出Symbol()的參數(shù)
        return Symbol(b); //用原先的Symbol()的參數(shù)創(chuàng)建一個新的Symbol
    }
    //拷貝正則類型數(shù)據(jù)
    function cloneReg(target) {
        const reFlags = /\w*$/;
        const result = new target.constructor(target.source, reFlags.exec(target));
        result.lastIndex = target.lastIndex;
        return result;
    }
    //拷貝函數(shù)
    function cloneFunction(targe) {
        //匹配函數(shù)體的正則
        const bodyReg = /(?<={)(.|\n)+(?=})/m;
        //匹配函數(shù)參數(shù)的正則
        const paramReg = /(?<=\().+(?=\)\s+{)/;
        const targeString = targe.toString();
        //利用prototype來區(qū)分下箭頭函數(shù)和普通函數(shù),箭頭函數(shù)是沒有prototype的
        if (targe.prototype) { //普通函數(shù)
            const param = paramReg.exec(targeString);
            const body = bodyReg.exec(targeString);
            if (body) {
                if (param) {
                    const paramArr = param[0].split(',');
                    //使用 new Function 重新構造一個新的函數(shù)
                    return new Function(...paramArr, body[0]);
                } else {
                    return new Function(body[0]);
                }
            } else {
                return null;
            }
        } else { //箭頭函數(shù)
            //eval和函數(shù)字符串來重新生成一個箭頭函數(shù)
            return eval(targeString);
        }
    }
    /**
     * 遍歷數(shù)據(jù)處理函數(shù)
     * @array 要處理的數(shù)據(jù)
     * @callback 回調(diào)函數(shù),接收兩個參數(shù) value 每一項的值 index 每一項的下標或者key。
     */
    function handleWhile(array, callback) {
        let index = -1;
        const length = array.length;
        while (++index < length) {
            callback(array[index], index);
        }
    }
    function clone(target, map) {
        if (isObject(target)) {
            let result = null;
            if (getType(target) === "[object Array]") {
                result = []
            } else if (getType(target) === "[object Object]") {
                result = {}
            } else if (getType(target) === "[object Map]") {
                result = new Map();
            } else if (getType(target) === "[object Set]") {
                result = new Set();
            }

            // 解決循環(huán)引用
            if (map.has(target)) {
                return map.get(target);
            }
            map.set(target, result);

            if (getType(target) === "[object Map]") {
                target.forEach((value, key) => {
                    result.set(key, clone(value, map));
                });
                return result;
            } else if (getType(target) === "[object Set]") {
                target.forEach(value => {
                    result.add(clone(value, map));
                });
                return result;
            } else if (getType(target) === "[object Object]" || getType(target) === "[object Array]") {
                const keys = getType(target) === "[object Array]" ? undefined : Object.keys(target);

                function callback(value, key) {
                    if (keys) {
                        // 如果keys存在則說明value是一個對象的key,不存在則說明key就是數(shù)組的下標。
                        key = value
                    }
                    result[key] = clone(target[key], map)
                }
                handleWhile(keys || target, callback)
            } else {
                result = handleOherData(target)
            }
            return result;
        } else {
            if (getType(target) === "[object Symbol]") {
                return cloneSymbol(target)
            } else {
                return target;
            }
        }
    }
    let map = new WeakMap;
    const result = clone(target, map);
    map = null;
    return result
}

在這里可以向面試官展示你的六個編程能力。

  • 代碼邏輯的嚴謹性。
  • 深入了解數(shù)據(jù)類型的能力。
  • JS Api 的熟練使用的能力。
  • 了解箭頭函數(shù)和普通函數(shù)的區(qū)別。
  • 熟練使用正則表達式的能力。
  • 模塊化開發(fā)的能力

王者段位

以上代碼中還有很多數(shù)據(jù)類型的拷貝,沒有實現(xiàn),有興趣的話可以在評論中實現(xiàn)一下,王者屬于你哦!

總結

綜上所述,面試官叫你實現(xiàn)一個深拷貝,其實是要考察你各方面的能力。例如

  • 白銀段位
    • 對原始類型和引用類型數(shù)據(jù)的判斷能力。
    • 對遞歸思維的應用的能力。
  • 黃金段位
    • 正確理解引用類型概念的能力。
    • 精確判斷數(shù)據(jù)類型的能力。
  • 鉑金段位
    • 對循環(huán)引用的理解,如何解決循環(huán)引用引起的問題的能力。
    • 熟悉 ES6 中 Map 數(shù)據(jù)結構的概念及應用。
    • 對內(nèi)存泄露的認識和避免泄露的能力。
  • 磚石段位
    • 熟悉 ES6 中 WeakMap 數(shù)據(jù)結構的概念及應用。
    • 具有優(yōu)化代碼運行性能的能力。
    • 了解遍歷的效率的能力。
    • 了解 ++ii++ 的區(qū)別。
    • 代碼抽象的能力。
  • 星耀段位
    • 代碼邏輯的嚴謹性。
    • 深入了解數(shù)據(jù)類型的能力。
    • JS Api 的熟練使用的能力。
    • 了解箭頭函數(shù)和普通函數(shù)的區(qū)別。
    • 熟練使用正則表達式的能力。
    • 模塊化開發(fā)的能力

所以不要去死記硬背一些手寫代碼的面試題,最好自己動手寫一下,看看自己達到那個段位了。

到此這篇關于前端面試的底氣之實現(xiàn)一個深拷貝的文章就介紹到這了,更多相關深拷貝實現(xiàn)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

最新評論