內功修煉之lodash——function系列(面試高頻考點)

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

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

能收穫什麼:面試

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

注意:編程

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

函數系列的整體難度比以前的數組、集合系列都要大一些。剛好,lodash函數系列的方法是面試中常常會問到的api

bind

  • _.bind(func, thisArg, [partials])建立一個函數 func,這個函數的 this 會被綁定在 thisArg。 而且任何附加在 _.bind 的參數會被傳入到這個綁定函數上。 這個 _.bind.placeholder 的值,默認是以 _ 做爲附加部分參數的佔位符。
  • 注意: 不一樣於原生的 Function#bind,這個方法不會設置綁定函數的 length 屬性。
  • 參數:func (Function)是要綁定的函數。thisArg ()的這個 this 會被綁定給 func。[partials] (...)指附加的部分參數
  • 返回值 (Function):新的綁定函數
  • 難度係數: ★★★★
  • 建議最長用時:12min
var greet = function(greeting, punctuation) {
  return greeting + ' ' + this.user + punctuation;
};

var object = { 'user': 'fred' };

var bound = _.bind(greet, object, 'hi');
bound('!');
// => 'hi fred!'

// 使用了佔位符
var bound = _.bind(greet, object, _, '!');// 填了第二個參數
bound('hi'); // 再傳第一個參數
// => 'hi fred!'

var bound = _.bind(greet, object, _, "!");// 填了第二個參數
bound(_, "hi")// 填了第2個參數,第一個參數`_`補上初始參數第一個空格,第二個參數hi接在後面
// => 'fred!'
複製代碼

參考代碼數組

const _ = {
  bind(f, thisArg, ...rest) {
    return function(...args) {
// 參數有空格,走新的邏輯
      return f.apply(thisArg, (rest.includes(_) || args.includes(_))? _.mergeArgs(rest, args):  [...rest, ...args])
    }
  },
  toString() {
    return '' // 隱式轉換
  },
  mergeArgs(init, args) {
    return init.map((arg) => {
// 執行時傳入的參數做爲填補
      if (arg === _ && args.length) {
        return args.shift()
      }
      return arg
    }).concat(args) // 剩下的參數都接到後面
  }
}
複製代碼

實現一個bind卻是很簡單,可是lodash的bind還有填空格的操做。把lodash本身填進去就是一個空格,並且咱們還能夠從console.log(bound(_, "hi"))發現,它具備隱式轉換:_ + '' === ''。實現lodash的bind的時候,除了兼容正常的函數bind以外,還要兼容傳入_的時候的空格的case,並merge初始化參數和調用時參數緩存

curry

正向柯里化

  • _.curry(func, [arity=func.length])建立一個函數,該函數接收一個或多個 func 的參數。 當該函數被調用時,若是 func 所須要傳遞的全部參數都被提供,則直接返回 func 所執行的結果。 不然繼續返回該函數並等待接收剩餘的參數。 可使用 func.length 強制須要累積的參數個數。
  • 這個 _.curry.placeholder 的值,默認是以 _ 做爲附加部分參數的佔位符。
  • 注意: 這個方法不會設置 "length" 到 curried 函數上。
  • 參數:func (Function)是須要 curry 的函數。[arity=func.length] (number)是指須要提供給 func 的參數數量
  • 返回 curry 後的函數
  • 難度係數: ★★★★★
  • 建議最長用時:15min
// example
var abc = function(a, b, c) {
  return [a, b, c];
};

var curried = _.curry(abc);

curried(1)(2)(3);
// => [1, 2, 3]

curried(1, 2)(3);
// => [1, 2, 3]

curried(1, 2, 3);
// => [1, 2, 3]

// 使用了佔位符
curried(1)(_, 3)(2);
// => [1, 2, 3]
複製代碼

參考代碼:閉包

const _ = {
  curry(f, arity = f.length) {
    return function(...initValues) {
// 每次執行,都是一個新的閉包,executes的位置要放這裏
      let executes = initValues
      function curried(...args) {
// 兼容空格
        const newArgs = _.mergeArgs(executes, args)
        executes = newArgs
// 過濾空格的真實長度
        if (_.getReallLength(newArgs) < arity) {
          return curried
        }
        const ret = f.apply(null, newArgs)
        return ret
      }
      return curried
    }
  },
  toString() {
    return ''
  },
  mergeArgs(init, args) {
// 有沒有空格
    if (!init.includes(_)) {
      return [...init, ...args]
    }
    return init.map((arg) => {
      if (arg === _ && args.length) {
        return args.shift()
      }
      return arg
    }).concat(args)
  },
  getReallLength(args) {
// 獲取真實長度
    return args.filter(arg => arg !== _).length
  }
}

function curry(f, arity = f.length) {
  const executes = []
  function curried(...args) {
    executes.push(...args)
    if (executes.length < arity) {
      return curried
    }
    const ret = f.apply(null, executes)
    executes.length = 0
    return ret
  }
  return curried
}
複製代碼

家喻戶曉的柯里化,可能不少人都會寫。可是在這裏還要考慮到lodash的空格以及柯里化函數屢次複用app

反向柯里化

原理同樣,只是取參數的時候從右邊往左邊取框架

  • 難度係數: ★★★★★★(若是已經實現里正向柯里化curry,難度降爲1星)
  • 建議最長用時:18min

example

var abc = function(a, b, c) {
  return [a, b, c];
};

var curried = _.curryRight(abc);

curried(3)(2)(1);
// => [1, 2, 3]

curried(2, 3)(1);
// => [1, 2, 3]

curried(1, 2, 3);
// => [1, 2, 3]

// 使用了佔位符
curried(3)(1, _)(2);
// => [1, 2, 3]
複製代碼

參考代碼:

// 只須要把上文的mergeArgs方法改一下便可
  _.mergeArgs = function(init, args) {
    if (!init.includes(_)) {
// 就改這裏,換個位置
      return [...init, ...args]
    }
    return init.map((arg) => {
      if (arg === _ && args.length) {
        return args.shift()
      }
      return arg
    }).concat(args)
  },
複製代碼

debounce

  • _.(func, [wait=0] debounce, [options])建立一個防抖動函數。 該函數會在 wait 毫秒後調用 func 方法。 該函數提供一個 cancel 方法取消延遲的函數調用以及 flush 方法當即調用。 能夠提供一個 options 對象決定如何調用 func 方法, options.leading 與|或 options.trailing 決定延遲先後如何觸發。 func 會傳入最後一次傳入的參數給防抖動函數。 隨後調用的防抖動函數返回是最後一次 func 調用的結果。
  • 注意: 若是 leading 和 trailing 都設定爲 true。 則 func 容許 trailing 方式調用的條件爲: 在 wait 期間屢次調用防抖方法。
  • 參數
    • func (Function) 要防抖動的函數
    • [wait=0] (number) 須要延遲的毫秒數
    • [options] (Object) 選項對象
    • [options.leading=false] (boolean) 指定調用在延遲開始前
    • [options.maxWait] (number) 設置 func 容許被延遲的最大值
    • [options.trailing=true] (boolean) 指定調用在延遲結束後
  • 返回值 (Function) 返回具備防抖動功能的函數
  • 難度係數: ★★★★★★
  • 建議最長用時:20min

我相信,80%的人能夠1分鐘內寫出trailing模式的debounce方法(定時器到了就執行函數,在定時器還沒到期間重複執行函數,定時器重置),可是同時支持options配置和leading模式的話,難度大大增長了

參考代碼:

// 執行方式:delay前、delay後、delay先後
function execute(f, timeout, ref, { isDelay, isDirectly }) {
  if (!ref.last && isDirectly) {
// 調用上一次保存下來的方法
    ref.isExecute = true
    ref.last = f
    f()
  }
  return setTimeout(() => {
    if (isDirectly) {
// 調用了就清掉
      ref.last = null
    }
    ref.isExecute = true
    if (isDelay) {
      f()
    }
  }, timeout);
}

function debounce(func, wait = 0, options = {}) {
  const { leading, maxWait, trailing = true } = options
  const ref = {
    t: undefined,
    isExecute: false, // 給maxWait用的標記
    maxWaitTimer: undefined,
    last: undefined, // leading模式用的
  }
  return function(...args) {
    const main = () => func.apply(null, args)
// 最大超時時間設置
    if ('maxWait' in options && !ref.maxWaitTimer) {
      ref.maxWaitTimer = setTimeout(() => {
        if (!ref.isExecute) {
          ref.maxWaitTimer = undefined
          return main()
        }
      }, maxWait);
    }
    clearTimeout(ref.t)
// 支持trailing、leading模式選擇
    ref.t = execute(main, wait, ref, { isDelay: trailing, isDirectly: leading })
  }
}
複製代碼

throttle

  • _.throttle(func, [wait=0], [options])建立一個節流函數,在 wait 秒內最多執行 func 一次的函數。 該函數提供一個 cancel 方法取消延遲的函數調用以及 flush 方法當即調用。 能夠提供一個 options 對象決定如何調用 func 方法, options.leading 與|或 options.trailing 決定 wait 先後如何觸發。 func 會傳入最後一次傳入的參數給這個函數。 隨後調用的函數返回是最後一次 func 調用的結果。
  • 注意: 若是 leading 和 trailing 都設定爲 true。 則 func 容許 trailing 方式調用的條件爲: 在 wait 期間屢次調用。
  • 參數:
    • func (Function) 要節流的函數
    • [wait=0] (number) 須要節流的毫秒
    • [options] (Object) 選項對象
    • [options.leading=true] (boolean) 指定調用在節流開始前
    • [options.trailing=true] (boolean) 指定調用在節流結束後
  • 返回值 (Function) 返回節流的函數
  • 難度係數: ★★★★★
  • 建議最長用時:15min

參考代碼:

function throttle(func, wait = 0, options = {}) {
  const { leading = true, maxWait, trailing } = options
  const ref = {
    t: undefined,
    isExecute: false,
    maxWaitTimer: undefined,
    last: undefined,
  }
  return function(...args) {
    const main = () => func.apply(null, args)
    if ('maxWait' in options && !ref.maxWaitTimer) {
      ref.maxWaitTimer = setTimeout(() => {
        if (!ref.isExecute) {
          ref.maxWaitTimer = undefined
          return main()
        }
      }, maxWait);
    }
    if (!ref.isExecute) {
      if (leading) {
        ref.isExecute = true
        main()
      }
      if (!ref.last && trailing) {
// 先記錄下等下trailing模式要執行的函數
        ref.last = main
      }
    }
    if (ref.t === undefined) {
      ref.t = setTimeout(() => {
// wait時間內只能執行一次
        ref.isExecute = false
        ref.t = undefined
        if (ref.last && trailing) {
// 執行記錄下來的函數
          ref.isExecute = true
          ref.last()
          ref.last = undefined
        }
      }, wait);
    }
  }
}
複製代碼

memoize

  • _.memoize(func, [resolver])建立一個會緩存 func 結果的函數。 若是提供了 resolver,就用 resolver 的返回值做爲 key 緩存函數的結果。 默認狀況下用第一個參數做爲緩存的 key。 func 在調用時 this 會綁定在緩存函數上。
  • 注意: 緩存會暴露在緩存函數的 cache 上。 它是能夠定製的,只要替換了 _.memoize.Cache 構造函數,或實現了 Map 的 delete, get, has, 以及 set方法。
  • 參數
    • func (Function) 須要緩存化的函數
    • [resolver] (Function) 這個函數的返回值做爲緩存的 key
    • 返回值 (Function) 返回緩存化後的函數
  • 難度係數: ★★
  • 建議最長用時:6min
// example
var object = { 'a': 1, 'b': 2 };
var other = { 'c': 3, 'd': 4 };

var values = _.memoize(_.values);
values(object);
// => [1, 2]

values(other);
// => [3, 4]

object.a = 2;
values(object);
// => [1, 2]

// 修改結果緩存
values.cache.set(object, ['a', 'b']);
values(object);
// => ['a', 'b']

// 替換 `_.memoize.Cache`
_.memoize.Cache = WeakMap;
複製代碼

參考代碼:

function memoize(func, resolver) {
  const cache = new Map()
  function f(...args) {
    const key = typeof resolver === 'function' ? resolver.apply(null, args) : args[0]
    if (!cache.get(key)) {
      const ret = func.apply(null, args)
      cache.set(key, ret)
      return ret
    } else {
      return cache.get(key)
    }
  }
  f.cache = cache
  return f
}

複製代碼

其餘

其餘方法都比較簡單,不須要20行代碼便可實現。須要注意的點是,執行傳入的函數的時候,要call、apply一下null,默認沒有this,這是基本操做。爲何呢?若是執行的那個函數內部依賴this,那傳入的必須是箭頭函數或者bind過this的函數。若是開發者傳入的不是箭頭函數或者bind過this的函數,框架代碼裏面執行傳入的函數的時候又沒有call、apply一下null的話,那框架自己就對業務代碼形成了污染了。另外,若是不依賴this,那爲什麼改他的this呢。咱們能夠看看丟失的this的例子:

// 內部依賴this的函數,不bind的話,this指向改變了致使報錯
const { getElementById } = document
getElementById('id')
// Uncaught TypeError: Illegal invocation

// 正確的作法
const getElementById = document.getElementById.bind(document)
getElementById('id')
複製代碼

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

相關文章
相關標籤/搜索