內功修煉之lodash——By、With系列

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

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

能收穫什麼:面試

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

概念:編程

  • SameValue標準: 目前已有等價的api——Object.is(a, b),表示a和b在SameValue標準下是否相等。Object.is和===不一樣的地方在於,能夠判斷NaN和NaN相等,可是0 和 -0是不相等
  • SameValueZero標準: 與SameValue差異僅僅在於,此標準下0和-0是相等的,Array.prototype.includes、Set.prototype.has內部就是使用SameValueZero

注意:api

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

準備工做

lodash數組方法裏面有好幾個函數是自己+By+With一組的。假設lodash裏面有一個函數foo,對應的有fooByfooWith方法。fooByfooWith方法多了一個參數,是對數據進行預處理的。fooBy最後一個參數能夠是函數、數組、字符串,若是是函數,則對前面數組參數每個元素進行預處理再執行真正的邏輯;若是是數組、字符串,則先調用_.property(lastArg)返回一個函數,使用該函數對前面數組參數每個元素進行預處理數組

// 都是同樣的
fooBy(arr, 'x');
fooWith(arr, _.property('x'));
fooWith(arr, item => item.x);
複製代碼

下文有_.property的簡單實現,詳細的等待後續更新ide

difference系列

difference

  • 描述: _.difference(array, [values]),建立一個差別化後的數組
  • 難度係數: ★
  • 建議最長用時:2min
_.difference([3, 2, 1], [4, 2]);
// => [3, 1]
複製代碼
_.difference([3, 2, 1], [4, 2]);
// => [3, 1]
複製代碼
參考代碼
function difference(origin, diff = []) {
  return origin.reduce(
    (acc, cur) => [...acc, ...(!diff.includes(cur) ? [cur] : [])],
    []
  );
}
複製代碼

differenceBy

  • 描述: _.differenceBy(array, [values], [iteratee=_.identity]),這個方法相似 _.difference,除了它接受一個 iteratee 調用每個數組和值。iteratee 會傳入一個參數:(value)。
  • 參數 array (Array): 須要處理的數組 [values] (...Array): 用於對比差別的數組 [iteratee=_.identity] (Function|Object|string): 這個函數會處理每個元素
  • 難度係數: ★ (不包括Object|string, 即differenceWith一樣的功能)
  • 建議最長用時:2min
// example
_.differenceBy([3.1, 2.2, 1.3], [4.4, 2.5], Math.floor);
// => [3.1, 1.3]

// 使用了 `_.property` (返回給定對象的 path 的值的函數)的回調結果
_.differenceBy([{ 'x': 2 }, { 'x': 1 }], [{ 'x': 1 }], 'x');
// => [{ 'x': 2 }]
複製代碼
參考代碼
function differenceBy(origin, diff = [], iteratee) {
  return origin.reduce(
    (acc, cur) => [...acc, ...(!~diff.findIndex(d => iteratee(d) === iteratee(cur))  ? [cur] : [])],
    []
  );
}
// 注意,使用`[].find`找到假值會致使誤判
複製代碼

property & differenceBy完整版

  • _.property(path) 建立一個返回給定對象的 path 的值的函數。參數: path (Array|string), 要獲得值的屬性路徑。differenceBy第三個參數實際上也使用了 _.property,下面實現differenceBy完整版
  • 難度係數: ★ ★ (iteratee包括Object|string)
  • 建議最長用時:6min
// example
var objects = [
  { 'a': { 'b': { 'c': 2 } } },
  { 'a': { 'b': { 'c': 1 } } }
];

_.map(objects, _.property('a.b.c'));
// => [2, 1]

_.map(_.sortBy(objects, _.property(['a', 'b', 'c'])), 'a.b.c');
// => [1, 2]
複製代碼
參考代碼
// 本文只實現一個簡單的_.property
function property(path) {
  return function(o) {
    const temp = Array.isArray(path) ? path : path.split(".");
    let res = o;
    while (temp.length && res) {
      res = res[temp.shift()];
    }
    return res;
  };
}

function differenceBy(origin, diff = [], iteratee) {
  iteratee = typeof iteratee === 'function' ? iteratee : property(iteratee);
  return origin.reduce(
    (acc, cur) => [...acc, ...(!~diff.findIndex(d => iteratee(d) === iteratee(cur))  ? [cur] : [])],
    []
  );
}
複製代碼

intersection系列

intersection

  • _.intersection([arrays]),建立一個包含全部使用 SameValueZero 進行等值比較後篩選的惟一值數組。
  • 參數: [arrays] (...Array), 須要處理的數組隊列;返回值 (Array): 全部數組共享元素的新數組
  • 難度係數: ★★★
  • 建議最長用時:9min
// example
_.intersection([2, 1], [4, 2], [1, 2]);
// => [2]
複製代碼
參考代碼
// 若是下一個組沒有上一個組的元素,那麼那個元素能夠pass了
// 用當前組的結果和上一組對比,再將當前組的結果覆蓋上一組的結果
// 使用一樣的方法遍歷每一組,最終結果便是交集

// 常規數組解法

function intersection(...arrs) {
  let temp;
  return arrs.reduce((acc, arr) => {
    if (!acc) { // 第一次進來,第一組作參照物
      return temp = arr
    }
// 展轉計算上一組結果
    return temp = arr.reduce((ret, item) => {
 // 上一組有本元素,且最終輸出結果內沒有本元素(保證去重)
      if (temp.includes(item)) {
 // 這裏if不能合在一塊兒使用&&,防止走到下面邏輯
        if (!ret.includes(item)) {
          ret.push(item);
        }
      } else {
// 發現本元素不在上一組的元素中,那就刪掉
        const idx = ret.findIndex(x => x === item)
        if (~idx) {
          ret.splice(idx, 1);
        }
      }
      return ret
    }, [])
  }, void 0)
}

// 對象映射解法
function intersection(...arrs) {
  let temp;
// 若是用Object.keys會轉字符串因此使用Object.values
  return Object.values(arrs.reduce((acc, arr) => {
    if (!acc) {
// 第一次初始化麻煩一些
      return temp = arr.reduce((o, c) => ({ ...o, [c]: c }), {})
    }
    return temp = arr.reduce((ret, item) => {
// 中間這裏查找有沒有存在, 不存在就從最終結果刪除
// 必定要in不能ret[item] 判斷,否則若是ret[item] 是假值就騙過這個if了
      if (item in temp) {
        ret[item] = item;
      } else if (ret[item]) {
        delete ret[item]; // in與delete配合美滋滋
      }
      return ret
    }, {})
  }, void 0))
}

// 高級解法
// Set內部使用SameValueZero加去重,無需擔憂NaN, -0
// Set增長刪除元素很方便,並且性能好
// 查詢是否存在某元素只須要O(1)時間
function intersection(...arrs) {
  let temp;
  return [...arrs.reduce((acc, arr) => {
    return temp = !acc ? new Set(arr) : arr.reduce((ret, item) => {
      ret[temp.has(item) ? 'add' : 'delete'](item)
      return ret
    }, new Set);
  }, void 0)]
}
複製代碼

intersectionBy

相似前面的differenceBy和differenceWith方法,基於上面的intersection方法修改,加上前面也說到了_.property的實現。intersectionBy除了它接受一個 iteratee 調用每個數組和值。iteratee 會傳入一個參數:(value)。函數式編程

  • 參數 array (Array): 須要處理的數組 [values] (...Array): 用於對比差別的數組 [iteratee=_.identity] (Function|Object|string): 這個函數會處理每個元素
  • 難度係數: ★★★ (不包括Object|string 的時候退化爲intersectionWith一樣的功能)
  • 建議最長用時:8min
// example
_.intersectionBy([2.1, 1.2], [4.3, 2.4], Math.floor);
// => [2.1]

// 使用了 `_.property` 的回調結果
_.intersectionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');
// => [{ 'x': 1 }]
複製代碼

可是也不是很簡單,前面的方案會致使_.intersectionBy([2.1, 1.2], [4.3, 2.4], Math.floor)返回的是2.4,因此須要改一下函數

參考代碼
// property繼續copy前面的
function property(path) {
  return function (o) {
    const temp = Array.isArray(path) ? path : path.split(".");
    let res = o;
    while (temp.length && res) {
      res = res[temp.shift()];
    }
    return res;
  };
}

// sameValueZero標準
function sameValueZero(a, b) {
  return (a === b) || (isNaN(a) && isNaN(b));
}

function intersectionBy(...arrs) {
  let iteratee = arrs.pop();
  iteratee = typeof iteratee === 'function' ? iteratee : property(iteratee);
  let temp;
  return Object.values(arrs.reduce((acc, arr) => {
    if (!acc) {
      return temp = arr.reduce((o, c) => ({ ...o, [c]: c }), {})
    }
    return temp = arr.reduce((ret, item) => {
      // 須要根據當前值iteratee一下,再從以前的值iteratee事後裏面找
      const prefix = iteratee(item);
      const newTemp = Object.values(temp);
      const newRet = Object.values(ret);
      const compare = t => sameValueZero(prefix, iteratee(t));
      if (newTemp.map(iteratee).includes(prefix)) {
        const preKey = newTemp.find(compare);
        if (preKey) {
          ret[preKey] = preKey;
        }
      } else if (newRet.map(iteratee).includes(prefix)) {
        const preKey = newRet.find(compare);
        if (preKey) {
          delete ret[preKey];
        }
      }
      return ret;
    }, {})
  }, void 0))
}

複製代碼

小結

最開始使用的方法,是以當前的結果去覆蓋前面的結果,因此致使intersectionBy最終結果取的是後面組的元素。所以,若是想實現lodash的intersectionBy,就要固定最開始的那一組,而後圍繞那一組開始走後面的邏輯。再次實現intersectionBy:性能

  • 難度係數: ★★★ (不包括Object|string 的時候退化爲intersectionWith一樣的功能)
  • 建議最長用時:8min

先實現普通的intersection

參考代碼
function intersection(...arrs) {
  return [...arrs.reduce((acc, arr) => {
// 遍歷set結構而不是遍歷每一組數組
    return !acc ? new Set(arr) : (acc.forEach(item => {
      if (!arr.includes(item)) {
        acc.delete(item)
      }
    }), acc)
  }, void 0)]
}
複製代碼

再稍微修改一下,實現intersectionBy,此時實現intersectionBy的難度是★

參考代碼
function intersectionBy(...arrs) {
  let iteratee = arrs.pop();
  iteratee = typeof iteratee === 'function' ? iteratee : property(iteratee);
  return [...arrs.reduce((acc, arr) => {
    return !acc ? new Set(arr) : (acc.forEach(item => {
// set結構轉化爲iteratee每個元素後再對比
      if (!arr.map(iteratee).includes(iteratee(item))) {
        acc.delete(item)
      }
    }), acc)
  }, void 0)]
}
複製代碼

union系列

union

  • _.union([arrays]), 建立順序排列的惟一值組成的數組。全部值通過 SameValueZero 等值比較。返回處理好的數組。實際上就是對全部的數組求並集
  • 難度係數: ★
  • 建議最長用時:2min
// example
_.union([2, 1], [4, 2], [1, 2]);
// => [2, 1, 4]
複製代碼
參考代碼
// 方法1
function union(...arrs) {
  return [...new Set(arrs.reduce((acc, arr) => [...acc, ...arr], []))]
}

// 方法2
function union(...arrs) {
  return arrs.reduce((acc, arr) =>
    [...acc, ...arr.filter(item => !acc.includes(item))], []);
}
複製代碼

unionBy

  • _.unionBy([arrays], [iteratee=_.identity])。這個方法相似 _.union,除了它接受一個 iteratee 調用每個數組和值。也是和上面的by、with同樣的
  • 難度係數: ★(_.property已經實現,能夠從上面copy)
  • 建議最長用時:2min
// example
_.unionBy([2.1, 1.2], [4.3, 2.4], Math.floor);
// => [2.1, 1.2, 4.3]

// 使用了 `_.property` 的回調結果
_.unionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');
// => [{ 'x': 1 }, { 'x': 2 }]
複製代碼

咱們在union的第二種方法基礎上改,很快就能夠實現

參考代碼
function unionBy(...arrs) {
  let iteratee = arrs.pop();
  iteratee = typeof iteratee === 'function' ? iteratee : property(iteratee);
  return arrs.reduce((acc, arr) =>
    [...acc, ...arr.filter(item => !acc.map(iteratee).includes(iteratee(item)))], []);
}
複製代碼

相似的就是uniq系列了,即數組去重,uniq只傳入一個數組進行去重。union傳入多個數組求並集,實際上就是合併全部的數組再去重,道理都是差很少的。有對象組成的數組那種完全去重,其實是_.uniqWith(arr, _.isEqual)_.isEqual是一個深度對比相等的方法,後續詳細展開

xor系列

  • _.xor([arrays])建立一個包含了全部惟一值的數組。使用了 symmetric difference 等值比較。
  • 參數: [arrays] (...Array)要處理的數組,返回值是所給數組的對等差分數組.
  • 難度係數: ★★
  • 建議最長用時:5min
// example
_.xor([2, 1], [4, 2]);
// => [1, 4]
複製代碼
參考代碼
// 方法1
function xor(...arrs) {
  return [...arrs.reduce((acc, arr) => 
    !acc ? new Set(arr) : arr.reduce((res, item) => 
      (res[res.has(item) ? 'delete' : 'add'](item), res)
    , acc)
  , void 0)]
}

// 方法2,比較容易擴展成By和With方法
function sameValueZero(a, b) {
  return a === b || (isNaN(a) && isNaN(b));
}

function xor(...arrs) {
  return arrs.reduce((acc, arr) => {
    arr.forEach(item => {
      const index = acc.findIndex(x => sameValueZero(x, item))
      if (!~index) {
        acc.push(item)
      } else {
        acc.splice(index, 1);
      }
    })
    return acc;
  }, [])
}
複製代碼

ok,到了這裏,xorByxorWith你們都知道怎麼作了吧

sortedIndex系列

sortedIndex

  • _.sortedIndex(array, value)使用二進制的方式(二分查找)檢索來決定 value 應該插入在數組中位置。它的 index 應該儘量的小以保證數組的排序。
  • 參數: array (Array) 是須要檢索的已排序數組,value (*)是要評估位置的值
  • 返回值 (number),返回 value 應該在數組中插入的 index。
  • 難度係數: ★★★
  • 建議最長用時:7min
// example
_.sortedIndex([30, 50], 40);
// => 1

_.sortedIndex([4, 5], 4);
// => 0
複製代碼
參考代碼
// 二分查找
function sortedIndex(arr, value) {
  let low = 0
  let high = arr == null ? low : arr.length

  while (low < high) {
    const mid = (low + high) >> 1
    const medium = arr[mid]
    if (medium !== null &&
      medium < value) {
      low = mid + 1
    } else {
      high = mid
    }
  }
  return high
}
複製代碼

sortedIndexBy也是一樣的道理,就很少說了。相對的,還有sortedLastIndex方法,只是它是反過來遍歷的:使用二進制的方式(二分查找)檢索來決定 value 應該插入在數組中位置。它的 index 應該儘量的以保證數組的排序。

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

相關文章
相關標籤/搜索