面試官:請你實現一個深克隆


面試官系列(2):既然React/Vue能夠用Event Bus進行組件通訊,你能夠實現下嗎?


前言

實現一個深克隆是面試中常見的問題的,但是絕大多數面試者的答案都是不完整的,甚至是錯誤的,這個時候面試官會不斷追問,看看你到底理解不理解深克隆的原理,不少狀況下一些只知其一;不知其二的面試者就原形畢漏了.javascript

咱們就來看一下如何實現一個深克隆,固然面試中沒有讓你完整實現的時候,可是你必定要搞清楚其中的坑在哪裏,才能夠輕鬆應對面試官的追問.java

在要實現一個深克隆以前咱們須要瞭解一下javascript中的基礎類型.node

javascript基礎類型   git

JavaScript原始類型:Undefined、Null、Boolean、Number、String、Symboles6

JavaScript引用類型:Objectgithub


1.淺克隆

  淺克隆之因此被稱爲淺克隆,是由於對象只會被克隆最外部的一層,至於更深層的對象,則依然是經過引用指向同一塊堆內存.面試

// 淺克隆函數
function shallowClone(o) {
  const obj = {};
  for ( let i in o) {
    obj[i] = o[i];
  }
  return obj;
}
// 被克隆對象
const oldObj = {
  a: 1,
  b: [ 'e', 'f', 'g' ],
  c: { h: { i: 2 } }
};

const newObj = shallowClone(oldObj);
console.log(newObj.c.h, oldObj.c.h); // { i: 2 } { i: 2 }
console.log(oldObj.c.h === newObj.c.h); // true

複製代碼

咱們能夠看到,很明顯雖然oldObj.c.h被克隆了,可是它還與oldObj.c.h相等,這代表他們依然指向同一段堆內存,這就形成了若是對newObj.c.h進行修改,也會影響oldObj.c.h,這就不是一版好的克隆.npm

newObj.c.h.i = 'change';
console.log(newObj.c.h, oldObj.c.h); // { i: 'change' } { i: 'change' }
複製代碼

咱們改變了newObj.c.h.i的值,oldObj.c.h.i也被改變了,這就是淺克隆的問題所在.api

固然有一個新的apiObject.assign()也能夠實現淺複製,可是效果跟上面沒有差異,因此咱們再也不細說了.數組

2.深克隆

2.1 JSON.parse方法

前幾年微博上流傳着一個傳說中最便捷實現深克隆的方法, JSON對象parse方法能夠將JSON字符串反序列化成JS對象,stringify方法能夠將JS對象序列化成JSON字符串,這兩個方法結合起來就能產生一個便捷的深克隆.

const newObj = JSON.parse(JSON.stringify(oldObj));
複製代碼

咱們依然用上一節的例子進行測試

const oldObj = {
  a: 1,
  b: [ 'e', 'f', 'g' ],
  c: { h: { i: 2 } }
};

const newObj = JSON.parse(JSON.stringify(oldObj));
console.log(newObj.c.h, oldObj.c.h); // { i: 2 } { i: 2 }
console.log(oldObj.c.h === newObj.c.h); // false
newObj.c.h.i = 'change';
console.log(newObj.c.h, oldObj.c.h); // { i: 'change' } { i: 2 }

複製代碼

果真,這是一個實現深克隆的好方法,可是這個解決辦法是否是太過簡單了.

確實,這個方法雖然能夠解決絕大部分是使用場景,可是卻有不少坑.

1.他沒法實現對函數 、RegExp等特殊對象的克隆

2.會拋棄對象的constructor,全部的構造函數會指向Object

3.對象有循環引用,會報錯

主要的坑就是以上幾點,咱們一一測試下.

// 構造函數
function person(pname) {
  this.name = pname;
}

const Messi = new person('Messi');

// 函數
function say() {
  console.log('hi');
};

const oldObj = {
  a: say,
  b: new Array(1),
  c: new RegExp('ab+c', 'i'),
  d: Messi
};

const newObj = JSON.parse(JSON.stringify(oldObj));

// 沒法複製函數
console.log(newObj.a, oldObj.a); // undefined [Function: say]
// 稀疏數組複製錯誤
console.log(newObj.b[0], oldObj.b[0]); // null undefined
// 沒法複製正則對象
console.log(newObj.c, oldObj.c); // {} /ab+c/i
// 構造函數指向錯誤
console.log(newObj.d.constructor, oldObj.d.constructor); // [Function: Object] [Function: person]

複製代碼

咱們能夠看到在對函數、正則對象、稀疏數組等對象克隆時會發生意外,構造函數指向也會發生錯誤。

const oldObj = {};

oldObj.a = oldObj;

const newObj = JSON.parse(JSON.stringify(oldObj));
console.log(newObj.a, oldObj.a); // TypeError: Converting circular structure to JSON
複製代碼

對象的循環引用會拋出錯誤.

2.2 構造一個深克隆函數

咱們知道要想實現一個靠譜的深克隆方法,上一節提到的序列/反序列是不可能了,而一般教程裏提到的方法也是不靠譜的,他們存在的問題跟上一屆序列反序列操做中凸顯的問題是一致的.

(這個方法也會出現上一節提到的問題)

因爲要面對不一樣的對象(正則、數組、Date等)要採用不一樣的處理方式,咱們須要實現一個對象類型判斷函數。

const isType = (obj, type) => {
  if (typeof obj !== 'object') return false;
  const typeString = Object.prototype.toString.call(obj);
  let flag;
  switch (type) {
    case 'Array':
      flag = typeString === '[object Array]';
      break;
    case 'Date':
      flag = typeString === '[object Date]';
      break;
    case 'RegExp':
      flag = typeString === '[object RegExp]';
      break;
    default:
      flag = false;
  }
  return flag;
};
複製代碼

這樣咱們就能夠對特殊對象進行類型判斷了,從而採用針對性的克隆策略.

const arr = Array.of(3, 4, 5, 2);

console.log(isType(arr, 'Array')); // true
複製代碼

對於正則對象,咱們在處理以前要先補充一點新知識.

咱們須要經過正則的擴展瞭解到flags屬性等等,所以咱們須要實現一個提取flags的函數.

const getRegExp = re => {
  var flags = '';
  if (re.global) flags += 'g';
  if (re.ignoreCase) flags += 'i';
  if (re.multiline) flags += 'm';
  return flags;
};
複製代碼

作好了這些準備工做,咱們就能夠進行深克隆的實現了.

/** * deep clone * @param {[type]} parent object 須要進行克隆的對象 * @return {[type]} 深克隆後的對象 */
const clone = parent => {
  // 維護兩個儲存循環引用的數組
  const parents = [];
  const children = [];

  const _clone = parent => {
    if (parent === null) return null;
    if (typeof parent !== 'object') return parent;

    let child, proto;

    if (isType(parent, 'Array')) {
      // 對數組作特殊處理
      child = [];
    } else if (isType(parent, 'RegExp')) {
      // 對正則對象作特殊處理
      child = new RegExp(parent.source, getRegExp(parent));
      if (parent.lastIndex) child.lastIndex = parent.lastIndex;
    } else if (isType(parent, 'Date')) {
      // 對Date對象作特殊處理
      child = new Date(parent.getTime());
    } else {
      // 處理對象原型
      proto = Object.getPrototypeOf(parent);
      // 利用Object.create切斷原型鏈
      child = Object.create(proto);
    }

    // 處理循環引用
    const index = parents.indexOf(parent);

    if (index != -1) {
      // 若是父數組存在本對象,說明以前已經被引用過,直接返回此對象
      return children[index];
    }
    parents.push(parent);
    children.push(child);

    for (let i in parent) {
      // 遞歸
      child[i] = _clone(parent[i]);
    }

    return child;
  };
  return _clone(parent);
};
複製代碼

咱們作一下測試

function person(pname) {
  this.name = pname;
}

const Messi = new person('Messi');

function say() {
  console.log('hi');
}

const oldObj = {
  a: say,
  c: new RegExp('ab+c', 'i'),
  d: Messi,
};

oldObj.b = oldObj;


const newObj = clone(oldObj);
console.log(newObj.a, oldObj.a); // [Function: say] [Function: say]
console.log(newObj.b, oldObj.b); // { a: [Function: say], c: /ab+c/i, d: person { name: 'Messi' }, b: [Circular] } { a: [Function: say], c: /ab+c/i, d: person { name: 'Messi' }, b: [Circular] }
console.log(newObj.c, oldObj.c); // /ab+c/i /ab+c/i
console.log(newObj.d.constructor, oldObj.d.constructor); // [Function: person] [Function: person]
複製代碼

固然,咱們這個深克隆還不算完美,例如Buffer對象、Promise、Set、Map可能都須要咱們作特殊處理,另外對於確保沒有循環引用的對象,咱們能夠省去對循環引用的特殊處理,由於這很消耗時間,不過一個基本的深克隆函數咱們已經實現了。


總結

實現一個完整的深克隆是由許多坑要踩的,npm上一些庫的實現也不夠完整,在生產環境中最好用lodash的深克隆實現.

在面試過程當中,咱們上面提到的衆多坑是面試官極可能追問你的,要知道坑在哪裏,能答出來纔是你的加分項,在面試過程當中必需要有一兩個閃光點,若是隻知道序列/反序列這種投機取巧的方法,在追問下不只拿不到分,極可能形成只懂個皮毛的印象,畢竟,面試面得就是你知識的深度.

相關文章
相關標籤/搜索