內功修煉之lodash—— clone&cloneDeep(必定有你遺漏的js基礎知識)

若是以爲沒有面試題,那麼lodash每個方法就能夠看成一個題目,能夠看着效果反過來實現,以不一樣的方法實現、多種方法實現,鞏固基礎。除了某些一瞬間就能夠實現的函數,下面抽取部分函數做爲試煉。時代在進步,下文全部的解法都採用es2015+前端

本文實現方法都是看效果倒推實現方法,並進行一些拓展和思考,和源碼無關。lodash這個庫在這裏更像一個題庫,給咱們刷題的node

能收穫什麼:es6

  • 修煉代碼基本功,瞭解常見的套路
  • 瞭解到一些操做的英文命名和規範
  • 積累經驗,面對複雜邏輯問題能夠迅速解決
  • 也許能夠查到本身的js基礎知識的漏洞

注意:面試

  • 三星難度以上的會具體拓展和講解
  • 文中使用的基本都是數組原生api以及es6+函數式編程,代碼簡潔且過程清晰
  • 若是說性能固然是命令式好,實現起來稍微麻煩一些並且比較枯燥無味
  • 時代在進步,人生苦短,我選擇語法糖和api。面臨大數據的性能瓶頸,纔是考慮命令式編程的時候

仍是老生常談的深淺拷貝,可是咱們此次完全探究一遍各類對象的拷貝以及補回一些js冷門知識編程

clone & cloneDeep(不考慮不經常使用對象)

lodash除了經常使用的數據類型拷貝外,還會對各類奇怪對象進行拷貝。在實現lodash的以前,咱們先實現一個正常的知足大部分場景的拷貝:api

淺拷貝

  • 難度係數: ★
  • 建議最長用時:2min
function shallowClone(v) {
const isArr = Array.isArray(v);
  if (typeof v === 'object' && v !== null && !isArr) {
    return { ...v } // ...包括symbol key
  }
  return isArr ? [ ...v ] : v
}
複製代碼

深拷貝

  • 難度係數: ★★★
  • 建議最長用時:9min
function deepCopy(target, cache = new Set()) {
const isArr = Array.isArray(target);
// 注意環引用
  if (
    (typeof target !== 'object' && target !== null && !isArr) ||
      cache.has(target)
    ) {
    return target
  }
  if (isArr) {
    return target.map(t => {
      cache.add(t)
      return t
    })
  } else {
// 注意symbol key
    return [...Object.keys(target), ...Object.getOwnPropertySymbols(target)].reduce((res, key) => {
      cache.add(target[key])
      res[key] = deepCopy(target[key], cache)
      return res
    }, target.constructor !== Object ? Object.create(target.constructor.prototype) : {}) // 繼承
  }
}
複製代碼

clone & cloneDeep(考慮各類對象)

  • _.clone(value)建立一個 value 的淺拷貝。_.cloneDeep(value)建立一個 value 的深拷貝。
  • 注意: 這個方法參考自 structured clone algorithm 以及支持 arrays、array buffers、 booleans、 date objects、maps、 numbers, Object objects, regexes, sets, strings, symbols, 以及 typed arrays。 參數對象的可枚舉屬性會拷貝爲普通對象。 一些不可拷貝的對象,例如error objects、functions, DOM nodes, 以及 WeakMaps 會返回空對象。
  • 源碼中,clone是baseClone(value, CLONE_SYMBOLS_FLAG),cloneDeep是baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG),此外,cloneDeepWith函數也是baseClone實現,此時baseClone基於還多了第三個參數customizer,是一個函數(customizer(value, key)),對深拷貝前的值作預處理。下面咱們要實現baseClone方法
  • 難度係數: ★★★★★★★★
  • 建議最長用時:25min

前置準備:CLONE_DEEP_FLAG, CLONE_SYMBOLS_FLAG這些是標記使用,或運算表示此次操做包含了哪些,傳到函數裏面會進行一次與操做,便可得出要作什麼數組

var CLONE_DEEP_FLAG = 1,  // 001
    CLONE_FLAT_FLAG = 2, // 010
    CLONE_SYMBOLS_FLAG = 4; // 100

// 若是bitmask包含了那位的1,那麼與操做後,那一位就是1,也就是true
var isDeep = bitmask & CLONE_DEEP_FLAG,
      isFlat = bitmask & CLONE_FLAT_FLAG,
      isFull = bitmask & CLONE_SYMBOLS_FLAG;
複製代碼

各類對象及類型

Buffer

爲了使 Buffer 實例的建立更可靠且更不容易出錯,各類形式的 new Buffer() 構造函數都已被棄用,且改成單獨的 Buffer.from(),Buffer.alloc() 和 Buffer.allocUnsafe() 方法。 Buffer的實例方法slice: buffer.slice([start[, end]]),返回一個新的 Buffer,它引用與原始的 Buffer 相同的內存,可是由 start 和 end 索引進行偏移和裁剪。安全

克隆buffer的方法:函數式編程

const allocUnsafe = Buffer ? Buffer.allocUnsafe : undefined;
function cloneBuffer(buffer, isDeep) {
  if (isDeep) {
    return buffer.slice(); // 深拷貝
  }
  const length = buffer.length;
  const result = allocUnsafe ? allocUnsafe(length) : new buffer.constructor(length);

// 爲何這就是淺拷貝呢?
// 其實和const o = { b: 1 }的淺拷貝是const a = new Object(); a.b = o.b一樣的道理
  buffer.copy(result);
  return result;
}
複製代碼

ArrayBuffer

ArrayBuffer 對象用來表示通用的、固定長度的原始二進制數據緩衝區。ArrayBuffer 不能直接操做,而是要經過類型數組對象或 DataView 對象來操做函數

function cloneArrayBuffer(arrayBuffer) {
// 先new一個同樣長度的
  const result = new arrayBuffer.constructor(arrayBuffer.byteLength);
// 使用Uint8Array操做ArrayBuffer,從新set一次
  new Uint8Array(result).set(new Uint8Array(arrayBuffer));
// 或者使用DataView
// new DataView(result).setUint8(new Uint8Array(arrayBuffer));
  return result;
}
複製代碼

DataView

上面提了一下dataview

function cloneDataView(dataView, isDeep) {
// 先把buffer拷貝
  const buffer = isDeep ? cloneArrayBuffer(dataView.buffer) : dataView.buffer;
// 再new 的時候,傳入拷貝過的buffer
  return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength);
}
複製代碼

其實dataview一些api和類型化數組(Float32Array, Float64Array, Int8Array, Int16Array, Int32Array, Uint8Array, Uint8ClampedArray, Uint16Array, Uint32Array)很像,套路都是同樣的,因此拷貝類型化數組直接改一下函數就ok

function cloneTypedArray(typedArray, isDeep) {
  const buffer = isDeep ? cloneArrayBuffer(typedArray.buffer) : typedArray.buffer;
  return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length);
}
複製代碼

RegExp

拷貝正則的時候,只須要讀取他的source以及其餘各類模式便可

/\w*$/.exec( /a/gim); // gim
複製代碼

所以克隆正則代碼也很簡單,可是要特別注意lastIndex在加了g的匹配模式下的坑:

image

function cloneRegExp(regexp) {
  const result = new regexp.constructor(regexp.source, /\w*$/.exec(regexp));
  result.lastIndex = regexp.lastIndex; // 有g的時候的坑
  return result;
}
複製代碼

Symbol

symbol類型的值,經過Symbol(value)產生,並且Symbol不能new。所以,克隆對象型的Symbol怎麼辦呢(如new Boolean、new Number這種手段產生的對象),其實只須要Object包一下便可,它的valueOf轉換仍是轉換爲正常的symbol類型的值

function cloneSymbol(symbol) {
  return Symbol.prototype.valueOf ? Object(symbol.valueOf()) : {};
}
複製代碼

其餘類型

如new出來的基本數據類型:Number、Boolean、String,也是直接從新new一下便可。Boolean須要注意:!!new Boolean(false) === true,可是+new Boolean(false) === 0,因此new時轉數字便可: new Boolean(+boolObj)

Date對象,也是直接new便可。不過爲了安全起見能夠先轉成數字時間戳再new

至於Set、Map,從新new一個空的,而後一個個加進去。須要把遞歸後的結果加進去,由於加進去的元素也多是複雜數據類型哦

數組克隆

數組初始化

初始化通常就定義一個空數組就好了。沒錯,的確是的。可是,還有一種特殊的數組,就是正則match返回的數組,具備index、input屬性:

function initCloneArray(array) {
  const length = array.length;
  const result = new array.constructor(length);
 
  // 正則match返回
  if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) {
    result.index = array.index;
    result.input = array.input;
  }
  return result;
}
複製代碼

接下來就是克隆一個數組了,淺拷貝就直接返回[...array],深拷貝數組的方法和普通對象同樣的

普通對象克隆

初始化對象initCloneObject

建立對象,咱們都知道一個花括號字面量便可:const o = {};。可是,咱們拷貝的時候,要考慮到繼承的狀況以及是否是原型對象

// 是不是原型對象
function isPrototype(value) {
  return value === (value.constructor.prototype || Object.prototype);
}

function initCloneObject(object) {
// 不是原型對象,那就Object.create保持繼承關係
// 是原型對象,那就直接返回一個空對象
  return (typeof object.constructor == 'function' && !isPrototype(object))
    ? Object.create(Object.getPrototypeOf(object))
    : {};
}
複製代碼

獲取全部的key

有了新對象,那麼下一步就是從舊對象裏面一個個key拷貝過來。對於key應該怎麼拿,有幾種case:

  • 拷貝symbol key和拷貝原型鏈
  • 不拷貝symbol key和拷貝原型鏈
  • 不拷貝symbol key和不拷貝原型鏈
  • 拷貝symbol key和不拷貝原型鏈

lodash裏面,isFlat表示是否拷貝原型鏈,isFull表示是否拷貝symbol key,keysFunc返回一個數組,給後面遍歷使用

const keysFunc = isFull
    ? (isFlat ? getAllKeysIn : getAllKeys)
    : (isFlat ? keysIn : keys);
複製代碼

根據咱們的目的,咱們要把4個獲取key的方法實現出來。可是,在實現以前,先鋪墊一下一些坑:

const o = { b: 1 }
Object.defineProperty(o, 'a', { value: 666, enumerable: false })
for(x in o) { console.log(x) } // 只有b
Object.keys(o) // 只有b

const sb = Symbol() // symbol 作key
Object.defineProperty(o, sb, { value: 666, enumerable: false })
Object.getOwnPropertySymbols(o) // Symbol
for(x in o) { console.log(x) } // 只有b
複製代碼

所以,實現代碼以下:

// 普通獲取key
function keys(object) {
  return Object.keys(object)
}
// 普通獲取key,包含symbol key
function getAllKeys(object) {
// getOwnPropertySymbols會把不能枚舉的都拿到
  return [
    ...Object.getOwnPropertySymbols(object).filter(key => object.propertyIsEnumerable(key)),
    ...Object.keys(object)
  ]
}

// 獲取原型鏈上的key
function keysIn(object) {
  const res = [];
  for (const key in object) {
    // 拷貝全部的屬性(除了最大的原型對象)
    if (key !== 'constructor' || (!isPrototype(object) && object.hasOwnProperty(key))) {
      result.push(key);
    }
  }
  return res;
}

function getAllKeysIn(object) {
  const res = [];
  // in拿不到symbol key
  for (const key in object) {
    // 拷貝全部的屬性(除了最大的原型對象)
    if (key !== 'constructor' || (!isPrototype(object) && object.hasOwnProperty(key))) {
      result.push(key);
    }
  }
  let temp = object;
  // 逐層獲取symbol key
  while(temp) {
    res.push(...Object.getOwnPropertySymbols(object).filter(key => object.propertyIsEnumerable(key)))
    temp = Object.getPrototypeOf(object)
  }
  return res;
}
複製代碼

遞歸賦值

一切準備就緒,就到了賦值階段:

// 僞代碼
function deepCopy(o, ...rest) {
  const result = {};
  if (isPlainObject) {
  keysFunc(o).forEach((key) => {
    result[key] = deepCopy(o[key], ...rest)
  });
  }
}
複製代碼

對於Map、Set都是差很少的,先new一個空的Set、Map,而後遍歷賦值,Set就使用add方法,Map使用set方法

所有代碼

鋪墊代碼
/* 常量定義 */
const CLONE_DEEP_FLAG = 1;
const CLONE_FLAT_FLAG = 2;
const CLONE_SYMBOLS_FLAG = 4;

globalThis.Buffer = globalThis.Buffer || undefined;

const argsTag = "[object Arguments]",
  arrayTag = "[object Array]",
  boolTag = "[object Boolean]",
  dateTag = "[object Date]",
  errorTag = "[object Error]",
  funcTag = "[object Function]",
  genTag = "[object GeneratorFunction]",
  mapTag = "[object Map]",
  numberTag = "[object Number]",
  objectTag = "[object Object]",
  regexpTag = "[object RegExp]",
  setTag = "[object Set]",
  stringTag = "[object String]",
  symbolTag = "[object Symbol]",
  weakMapTag = "[object WeakMap]";

const arrayBufferTag = "[object ArrayBuffer]",
  dataViewTag = "[object DataView]",
  float32Tag = "[object Float32Array]",
  float64Tag = "[object Float64Array]",
  int8Tag = "[object Int8Array]",
  int16Tag = "[object Int16Array]",
  int32Tag = "[object Int32Array]",
  uint8Tag = "[object Uint8Array]",
  uint8ClampedTag = "[object Uint8ClampedArray]",
  uint16Tag = "[object Uint16Array]",
  uint32Tag = "[object Uint32Array]";

/* 初始化系列 */
// 初始化數組
function initCloneArray(array) {
  const length = array.length;
  const result = new array.constructor(length);
  // 正則match返回
  if (
    length &&
    typeof array[0] == "string" &&
    hasOwnProperty.call(array, "index")
  ) {
    result.index = array.index;
    result.input = array.input;
  }
  return result;
}

// 初始化普通對象
// 是不是原型對象
function isPrototype(value) {
  return value === (value.constructor.prototype || Object.prototype);
}

function initCloneObject(object) {
  // 不是原型對象,那就Object.create保持繼承關係
  // 是原型對象,那就直接返回一個空對象
  return typeof object.constructor == "function" && !isPrototype(object)
    ? Object.create(Object.getPrototypeOf(object))
    : {};
}

// 獲取[Object xxx]
function getTag(v) {
  return Object.prototype.toString.call(v);
}
// 普通獲取key
function keys(object) {
  return Object.keys(object);
}
// 普通獲取key,包含symbol key
function getAllKeys(object) {
  // getOwnPropertySymbols會把不能枚舉的都拿到
  return [
    ...Object.getOwnPropertySymbols(object).filter(key =>
      object.propertyIsEnumerable(key)
    ),
    ...Object.keys(object)
  ];
}

// 獲取原型鏈上的key
function keysIn(object) {
  const res = [];
  for (const key in object) {
    // 拷貝全部的屬性(除了最大的原型對象)
    if (
      key !== "constructor" ||
      (!isPrototype(object) && object.hasOwnProperty(key))
    ) {
      result.push(key);
    }
  }
  return res;
}

function getAllKeysIn(object) {
  const res = [];
  // in拿不到symbol key
  for (const key in object) {
    // 拷貝全部的屬性(除了最大的原型對象)
    if (
      key !== "constructor" ||
      (!isPrototype(object) && object.hasOwnProperty(key))
    ) {
      result.push(key);
    }
  }
  let temp = object;
  // 逐層獲取symbol key
  while (temp) {
    res.push(
      ...Object.getOwnPropertySymbols(object).filter(key =>
        object.propertyIsEnumerable(key)
      )
    );
    temp = Object.getPrototypeOf(object);
  }
  return res;
}

/* 克隆系列 */
// 克隆Buffer
const allocUnsafe = Buffer ? Buffer.allocUnsafe : undefined;
function cloneBuffer(buffer, isDeep) {
  if (isDeep) {
    return buffer.slice(); // 深拷貝
  }
  const length = buffer.length;
  const result = allocUnsafe
    ? allocUnsafe(length)
    : new buffer.constructor(length);

  // 爲何這就是淺拷貝呢?
  // 其實和const o = { b: 1 }的淺拷貝是const a = new Object(); a.b = o.b一樣的道理
  buffer.copy(result);
  return result;
}

// 克隆ArrayBuffer
function cloneArrayBuffer(arrayBuffer) {
  // 先new一個同樣長度的
  const result = new arrayBuffer.constructor(arrayBuffer.byteLength);
  // 使用Uint8Array操做ArrayBuffer,從新set一次
  new Uint8Array(result).set(new Uint8Array(arrayBuffer));
  // 或者使用DataView
  // new DataView(result).setUint8(new Uint8Array(arrayBuffer));
  return result;
}

// 克隆dataview
function cloneDataView(dataView, isDeep) {
  // 先把buffer拷貝
  const buffer = isDeep ? cloneArrayBuffer(dataView.buffer) : dataView.buffer;
  // new的時候,傳入拷貝過的buffer
  return new dataView.constructor(
    buffer,
    dataView.byteOffset,
    dataView.byteLength
  );
}

// 克隆類型化數組
function cloneTypedArray(typedArray, isDeep) {
  const buffer = isDeep
    ? cloneArrayBuffer(typedArray.buffer)
    : typedArray.buffer;
  return new typedArray.constructor(
    buffer,
    typedArray.byteOffset,
    typedArray.length
  );
}

// 克隆正則對象
function cloneRegExp(regexp) {
  const result = new regexp.constructor(regexp.source, /\w*$/.exec(regexp));
  result.lastIndex = regexp.lastIndex; // 有g的時候的坑
  return result;
}

// 一些對象的初始化或者克隆
function initCloneByTag(object, tag, isDeep) {
  const Ctor = object.constructor;
  switch (tag) {
    case arrayBufferTag:
      return cloneArrayBuffer(object);

    case boolTag:
    case dateTag:
      return new Ctor(+object);

    case dataViewTag:
      return cloneDataView(object, isDeep);

    case float32Tag:
    case float64Tag:
    case int8Tag:
    case int16Tag:
    case int32Tag:
    case uint8Tag:
    case uint8ClampedTag:
    case uint16Tag:
    case uint32Tag:
      return cloneTypedArray(object, isDeep);

    case mapTag:
      return new Ctor();

    case numberTag:
    case stringTag:
      return new Ctor(object);

    case regexpTag:
      return cloneRegExp(object);

    case setTag:
      return new Ctor();

    case symbolTag:
      return Symbol.prototype.valueOf ? Object(object.valueOf()) : {};
  }
}
複製代碼

baseClone主邏輯:

function baseClone(value, bitmask, customizer, key, object, cache = new Set()) {
  const isDeep = bitmask & CLONE_DEEP_FLAG; // 是否深拷貝
  const isFlat = bitmask & CLONE_FLAT_FLAG; // 是否拷貝原型鏈上的屬性
  const isFull = bitmask & CLONE_SYMBOLS_FLAG; // 是否拷貝symbol key
  const type = typeof value;
  const isArr = Array.isArray(value);
  let result;

  // cloneWith會用到的customizer
  if (customizer) {
    // object是遞歸帶過來的
    result = object ? customizer(value, key, object, cache) : customizer(value);
  }
  if (result !== undefined) {
    return result;
  }
  // 不是複雜類型直接返回
  if (!(value !== null && (type === "object" || type === "function"))) {
    return value;
  }

  if (isArr) {
    // 克隆數組
    result = initCloneArray(value);
    if (!isDeep) {
      // 淺拷貝,直接連上去
      return result.concat(value);
    }
  } else {
    // 克隆其餘
    const tag = getTag(value);
    // 是否是函數,按照規範,函數不克隆
    const isFunc = tag == funcTag || tag == genTag;
    // 克隆Buffer
    if (Buffer && Buffer.isBuffer(value)) {
      return cloneBuffer(value, isDeep);
    }
    // 普通對象、argument、函數
    if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
      // 初始化對象
      result = isFlat || isFunc ? {} : initCloneObject(value);
      // 淺拷貝
      if (!isDeep) {
        // 是否獲取原型鏈上的symbol key和普通key
        const getKeysFunc = isFlat ? getAllKeysIn : getAllKeys;
        return getKeysFunc(value).reduce((acc, shallowCopyKey) => {
          // 一個個賦值
          acc[shallowCopyKey] = value[shallowCopyKey];
          return acc;
        }, {});
      }
    } else {
      // 不能拷貝的對象就放棄
      if (!cloneableTags[tag]) {
        return object ? value : {};
      }
      // arrayBuffer、typedarray、dataView、regexp、Object{[基本數據類型]}的拷貝
      // set、map在這裏只是初始化一個空的
      result = initCloneByTag(value, tag, isDeep);
    }
  }

  // 檢查環引用
  const cacheed = cache.has(value);
  if (cacheed) {
    return cacheed;
  }
  cache.add(value, result);

  // set和map,一個個來
  if (isSet(value)) {
    value.forEach(subValue => {
      result.add(
        baseClone(subValue, bitmask, customizer, subValue, value, cache)
      );
    });
  } else if (isMap(value)) {
    value.forEach((subValue, key) => {
      result.set(
        key,
        baseClone(subValue, bitmask, customizer, key, value, cache)
      );
    });
  }

  // 獲取key的函數
  const keysFunc = isFull
    ? isFlat
      ? getAllKeysIn
      : getAllKeys
    : isFlat
    ? keysIn
    : keys;

  // 對象的屬性,只有普通對象有props
  const props = isArr ? undefined : keysFunc(value);
  (props || value).forEach((subValue, key) => {
    let newKey = key; // 數組的index或者對象的key
    let newValue = subValue; // subValue原本是所拷貝的對象裏面的key或者數組的一個元素值
    // 是對象的時候
    if (props) {
      newKey = newValue; // 若是是對象,新的key便是forEach第一個參數
      newValue = value[newKey]; // 所拷貝的對象裏面的key對應的value
    }
    // 賦值,遞歸還把當前的一些值帶下去(value、cache、newKey)
    result[newKey] = baseClone(
      newValue,
      bitmask,
      customizer,
      newKey,
      value,
      cache
    );
  });
  return result;
}
複製代碼

關注公衆號《不同的前端》,以不同的視角學習前端,快速成長,一塊兒把玩最新的技術、探索各類黑科技

相關文章
相關標籤/搜索