你以為面試官在問深拷貝的時候,僅僅是在問深拷貝嗎?

王亮hengg發表於2020-07-29

deep.001.jpeg

深拷貝可以說是前端面試中非常高頻的問題,也是一道基礎題。所謂的基礎不是說深拷貝本身是一個非常簡單、非常基礎的問題,而是面試官要通過深拷貝來考察候選人的JavaScript基礎,甚至是程式設計能力。

為什麼需要深拷貝?

第一個問題,也是最淺顯的問題,為什麼 JavaScript 中需要深拷貝?或者說如果不使用深拷貝複製物件會帶來哪些問題?

我們知道在 JavaScript 中存在“引用型別“和“值型別“的概念。因為“引用型別“的特殊性,導致我們複製物件不能通過簡單的clone = target,所以需要把原物件的屬性值一一賦給新物件。

而物件的屬性其值也可能是另一個物件,所以我們需要遞迴

如何獲取原物件的屬性?

通過for...in能夠遍歷物件上的屬性;也可以通過Object.keys(target)獲取到物件上的屬性陣列後再進行遍歷。
這裡選用for...in因為相比Object.keys(target)它還會遍歷物件原型鏈上的屬性。

ES6 Symbol 型別也可以作為物件的 key ,如何獲取它們?

如何判斷物件的型別?

可以使用typeof判斷目標是否為引用型別,這裡有一處需要注意:typeof null也是object

function deepClone(target) {
    const targetType = typeof target;
    if (targetType === 'object' || targetType === 'function') {
        let clone = Array.isArray(target)?[]:{}
        for (const key in target) {
            clone[key] = deepClone(target[key])
        }
        return clone;
    }
    return target;
}

上述程式碼就完成了一個非常基礎的深拷貝。但是對於引用型別的處理,它仍然是不完善的:

它沒法處理Date或者正則這樣的物件。為什麼?

“回字的四樣寫法“--具體型別的識別

獲取一個物件具體型別有哪些方式?

常用的方式有target.constructor.nameObject.prototype.toString.call(target)instanceOf

  • instacneOf可以用來判斷物件型別,但是Date的例項同時也是Object的例項,此處用於判斷是不準確的;
  • target.constructor.name得到的是構造器名稱,而構造器是可以被修改的;
  • Object.prototype.toString.call(target)返回的是類名,而在ES5中只有內建型別物件才有類名。

所以此處我們最合適的選擇是Object.prototype.toString.call(target)

Object.prototype.toString.call(target)也存在一些問題,你知道嗎?

稍微改進一下程式碼,做一些簡單的型別判斷:

function deepClone(target) {
    const targetType = typeof target;
    if (targetType === 'object' || targetType === 'function') {
        let clone = Array.isArray(target)?[]:{};

        if(Object.prototype.toString.call(target) === '[object Date]'){
            clone = new Date(target)
        }
        
        if(Object.prototype.toString.call(target) === '[object Object]'
        ||Object.prototype.toString.call(target) === '[object Array]'){
            for (const key in target) {
                clone[key] = deepClone(target[key])
            }
        }

        return clone;
    }
    return target;
}

怎麼能夠更優雅的做型別判斷?

你聽說過“迴圈引用“嗎?

假如目標物件的屬性間接或直接的引用了自身,就會形成迴圈引用,導致在遞迴的時候爆棧。
所以我們的程式碼需要迴圈檢測,設定一個Map用於儲存已拷貝過的物件,當檢測到物件已存在於Map中時,取出該值並返回即可避免爆棧。

function deepClone(target, map = new Map()) {
    const targetType = typeof target;
    if (targetType === 'object' || targetType === 'function') {
        let clone = Array.isArray(target)?[]:{};
        if (map.get(target)) {
            return map.get(target);
        }
        
        map.set(target, clone);

        if(Object.prototype.toString.call(target) === '[object Date]'){
            clone = new Date(target)
        }
        
        if(Object.prototype.toString.call(target) === '[object Object]'
            ||Object.prototype.toString.call(target) === '[object Array]'){
            for (const key in target) {
                clone[key] = deepClone(target[key],map)
            }
        }

        return clone;
    }
    return target;
}

好多教程使用 WeakMap 做儲存,相比Map,WeakMap好在哪兒?

通往優秀的階梯

以上我們就完成了一個基礎的深拷貝。但是它僅僅是及格而已,想要做到優秀,還要處理一下之前留下的幾個問題。

獲取Symbol屬性

ES6Symbol型別也可以作為物件的 key ,但是for...inObject.keys(target)都拿不到 Symbol型別的屬性名。

好在我們可以通過Object.getOwnPropertySymbols(target) 獲取物件上所有的Symbol屬性,再結合for...inObject.keys()就能夠拿到全部的 key。不過這種方式有些麻煩,有沒有更好用的方法?

有!Reflect.ownKeys(target) 正是這樣一個集優雅與強大與一身的方法。但是正如同人無完人,這個方法也不完美:顧名思義,ownKeys是拿不到原型鏈上的屬性的。所以需要結合具體場景來組合使用上述方法。

特殊的內建型別

DateError等特殊的內建型別雖然是物件,但是並不能遍歷屬性,所以針對這些型別需要重新呼叫對應的構造器進行初始化。JavaScript 內建了許多類似的特殊型別,然而我們並不是無情的 API 機器,面試中能夠回答上述要點也就足夠了。

上述內建型別我們都可以通過Object.prototype.toString.call(target) 的方式拿到,所以這裡可以封裝一個型別判斷的方法用於判斷target 是否能夠繼續遍歷,以便於及後續的處理。

然而 ES6 新增了Symbol.toStringTag方法,可以用來自定義類名,這就導致 Object.prototype.toString.call(target)拿到的型別名也可能不夠準確:

class ValidatorClass {
  get [Symbol.toStringTag]() {
    return "Validator";
  }
}

Object.prototype.toString.call(new ValidatorClass()); 
// "[object Validator]"

使用WeakMap做迴圈檢測,比使用Map好在哪兒?

原生的WeakMap持有的是每個鍵物件的“弱引用”,這意味著在沒有其他引用存在時垃圾回收能正確進行。如果 target 非常龐大,那麼使用Map 後如果沒有進行手動釋放,這塊記憶體就會持續的被佔用。而WeakMap則不需要擔心這個問題。

後記

如果上面幾個問題都得到了妥善的處理,那麼這樣的深拷貝就可以說是一個足夠打動面試官的深拷貝了。當然這個深拷貝還不夠優秀,有很多待完善的地方,相信善於思考的你已經有了自己的思路。

但本文的重點並不單單是實現一個深拷貝,更多的是希望它能夠幫助你更好的理解面試官的思路,從而更好的發揮自身的能力。

參考資料

關注「JS漫步指南」公眾號,獲取更多面試祕籍!

相關文章