Lodash 源碼分析(二)「Function」 Methods

前言

這是Lodash源碼分析的第二篇文章,咱們在第一篇Lodash 源碼分析(一)「Function」 Methods中介紹了基本的_.after_.map,以及複雜的_.ary函數的實現以及咱們本身的自定義輕量級版本。大概清楚了Lodash的整個代碼脈絡。此次咱們繼續分析,此次咱們講講_.reduce_.curryjavascript

_.reduce

我一直以爲,若是可以理解_.map_.reduce的實現,那麼任何複雜的函數都不在話下。咱們已經介紹了_.map的實現,知道了_.map函數中是如何處理集合,並將其逐個進行函數處理的。咱們知道在_.map函數中會把三個參數傳到給定的函數中,分別是array[index]indexarray。此次咱們看看_.reduce函數。java

衆所周知,_.reduce函數可以將一個集合進行"摺疊"。"摺疊"理解起來比較抽象。咱們能夠經過代碼做爲樣例說明一下:編程

const _ = require("lodash");
_.reduce([1,2,3],function(a,b){return a+b});
// 6

若是你不知道_.reduce究竟是怎麼工做的,那麼你能夠看看我寫的這篇文章從Haskell、JavaScript、Go看函數式編程。咱們今天的目的是看看lodash是如何實現_.reduce的,以及和咱們函數式的實現的區別。segmentfault

咱們看到lodash源代碼是這樣的:數組

function reduce(collection, iteratee, accumulator) {
var func = isArray(collection) ? arrayReduce : baseReduce,
    initAccum = arguments.length < 3;

  return func(collection, getIteratee(iteratee, 4), accumulator, initAccum, baseEach);
}

在官方的註釋中說,對於對象,遍歷順序是沒法保證的。咱們不考慮這麼複雜的狀況,先看看Array的狀況。其次,咱們在調用_.reduce的時候沒有傳入第三個accumulator參數,那麼函數能夠簡化爲:性能優化

function reduce(collection, iteratee, accumulator) {
  return arrayReduce(collection, getIteratee(iteratee, 4), accumulator, true, baseEach);
}

在看看arrayReduce函數:閉包

function arrayReduce(array, iteratee, accumulator, initAccum) {
    var index = -1,
        length = array == null ? 0 : array.length;

    if (initAccum && length) {
      accumulator = array[++index];
    }
    while (++index < length) {
      accumulator = iteratee(accumulator, array[index], index, array);
    }
    return accumulator;
  }

這裏的accumulator是初始累加值,若是傳入,則"摺疊"在其基礎上進行,就上面的最簡單的例子而言,若是傳入第三個參數是2,那麼返回值就會使8app

const _ = require("lodash");
_.reduce([1,2,3],function(a,b){return a+b},8);
// 8

因此arrayReduce函數就是給定一個初始值而後進行迭代的函數。咱們真正須要關注的函數式iteratee函數,即getIteratee(func, 4)這裏的func就是我進行重命名以後的自定義函數。函數式編程

這個getIteratee函數在介紹_.map的時候就進行介紹了,在func是一個function的狀況下,就是返回func自己。函數

因此咱們能夠把整個reduce函數簡化爲以下版本:

function reduce(array, func, accumulator) {
    var index = -1,
        length = array == null ? 0 : array.length;
    if (length) {
      accumulator = array[++index];
    }
    while (++index < length) {
      accumulator = func(accumulator, array[index], index, array);
    }
    return accumulator;
  }

其實看上去很像一個」遞歸「函數,由於前面一次的運算結果將會用於下一次函數調用,但又不是遞歸函數。咱們其實徹底能夠寫一個遞歸版本的reduce

function reduce(array,func,accumulator){
  accumulator = accumulator == null ? array[0]:accumulator;
  if (array.length >0){
    var a = array.shift();
    accumulator = func(a,accumulator);
    return reduce(array,func,accumulator);
  }
  return accumulator
}

工做的也不錯,但在分析過程當中,發現lodash一直在避免修改原參數的值,儘可能讓整個函數調用時無反作用的。我以爲這個思想在開發過程當中也有不少值得借鑑的地方。

_.curry

瞭解過函數式編程的同窗必定聽過大名鼎鼎的柯里化,在Lodash中也有一個專門用於柯里化的函數_.curry。這個函數接受一個函數func和這個函數的部分參數,而後返回一個接受剩餘參數的函數func'

咱們看看這個函數是怎麼實現的:

function curry(func, arity, guard) {
   arity = guard ? undefined : arity;
   var result = createWrap(func, WRAP_CURRY_FLAG, undefined, undefined, undefined, undefined, undefined, arity);
   result.placeholder = curry.placeholder;
   return result;
}

咱們又看到咱們的老朋友createWrap了,其實這個函數咱們在上一篇文章中分析過,可是咱們那時候是分析_.ary函數的時候進行了精簡,此次咱們看看createWrap函數式怎麼對_.curry函數進行處理的(將無關邏輯進行精簡):

function createWrap(func, bitmask, thisArg, partials, holders, argPos, ary, arity) {
      var isBindKey = 0
      var length =  0;
      ary = undefined ;
      arity = arity === undefined ? arity : toInteger(arity);
      length -= holders ? holders.length : 0;
      var data = getData(func);
      var newData = [
        func, bitmask, thisArg, partials, holders, partialsRight, holdersRight,
        argPos, ary, arity
      ];

      if (data) {
        mergeData(newData, data);
      }
      func = newData[0];
      bitmask = newData[1];
      thisArg = newData[2];
      partials = newData[3];
      holders = newData[4];
      arity = newData[9] = newData[9] === undefined
        ? func.length
        : nativeMax(newData[9] - length, 0);
      result = createCurry(func, bitmask, arity);
      var setter = data ? baseSetData : setData;
      return setWrapToString(setter(result, newData), func, bitmask);
    }

這裏面的關鍵就是createCurry函數了:

function createCurry(func, bitmask, arity) {
      var Ctor = createCtor(func);

      function wrapper() {
        var length = arguments.length,
            args = Array(length),
            index = length,
            placeholder = getHolder(wrapper);

        while (index--) {
          args[index] = arguments[index];
        }
        var holders = (length < 3 && args[0] !== placeholder && args[length - 1] !== placeholder)
          ? []
          : replaceHolders(args, placeholder);

        length -= holders.length;
        if (length < arity) {
          return createRecurry(
            func, bitmask, createHybrid, wrapper.placeholder, undefined,
            args, holders, undefined, undefined, arity - length);
        }
        var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func;
        return apply(fn, this, args);
      }
      return wrapper;
    }

不得不說和createHybird函數十分類似,可是其中還有一個比較關鍵的函數,就是createRecurry,這個函數返回了一個可以繼續進行curry的函數:

function createRecurry(func, bitmask, wrapFunc, placeholder, thisArg, partials, holders, argPos, ary, arity) {
      var isCurry = bitmask & WRAP_CURRY_FLAG,
          newHolders = isCurry ? holders : undefined,
          newHoldersRight = isCurry ? undefined : holders,
          newPartials = isCurry ? partials : undefined,
          newPartialsRight = isCurry ? undefined : partials;

      bitmask |= (isCurry ? WRAP_PARTIAL_FLAG : WRAP_PARTIAL_RIGHT_FLAG);
      bitmask &= ~(isCurry ? WRAP_PARTIAL_RIGHT_FLAG : WRAP_PARTIAL_FLAG);

      if (!(bitmask & WRAP_CURRY_BOUND_FLAG)) {
        bitmask &= ~(WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG);
      }
      var newData = [
        func, bitmask, thisArg, newPartials, newHolders, newPartialsRight,
        newHoldersRight, argPos, ary, arity
      ];

      var result = wrapFunc.apply(undefined, newData);
      if (isLaziable(func)) {
        setData(result, newData);
      }
      result.placeholder = placeholder;
      return setWrapToString(result, func, bitmask);
    }

Lodash爲了實現curry化,進行了多層的包裝,爲了實現返回的是劃一的Lodash中定義的可以curry化的函數。

這個函數要求接受相應的參數列表,即代碼中的data。在curry化的過程當中有一個很是重要的東西,就是佔位符placeholder。在對curry化的函數進行調用時也能夠用佔位符進行佔位:

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 with placeholders.
curried(1)(_, 3)(2);
// => [1, 2, 3]

能夠用下劃線_做爲佔位符佔位。咱們且不看lodash爲咱們作的不少複雜的預處理和特殊狀況的處理,咱們就分析_.curry函數實現的主要思想。首先_.curry函數有一個屬性存儲了最初的函數的接受函數參數的個數。而後有一個參數數組用於存儲部分參數,若是參數個數沒有知足調用函數須要的個數,就繼續返回一個從新curry化的函數。

根據上面的思想咱們能夠寫出一個簡化的curry化代碼:

/**
 *
 *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 with placeholders.
 * curried(1)("_", 3)(2)
 * 這就沒法處理了
 * // => [1, 3, 2]
 */

function curry(func){
  function wrapper(){
    func.prototype.that = func.prototype.that ? func.prototype.that : this;
    func.prototype.paramlength = func.prototype.paramlength ? func.prototype.paramlength: func.length ;
    func.prototype.paramindex = func.prototype.paramindex ?func.prototype.paramindex : 0;
    func.prototype.paramplaceholder = func.prototype.paramplaceholder ?  func.prototype.paramplaceholder : Array(func.length);
    for (var i = 0 ; i < arguments.length; i++) {
      if (arguments[i] == '_'){
        continue;
      }else{
        func.prototype.paramplaceholder[func.prototype.paramindex] = arguments[i];
        func.prototype.paramindex += 1;
      }
    }
    if (func.prototype.paramindex == func.prototype.paramlength){
      func.prototype.paramindex = 0;
      return func.apply(func.prototype.that,func.prototype.paramplaceholder)
    }
    return wrapper;
  }
  return wrapper;
}

咱們雖然能夠藉助Lodash的思想實現咱們一個簡單版本的curry函數,可是這個簡單版本的函數有一個問題,那就是,這個函數是藉助閉包實現的,在整個執行過程中,只要被柯里化的函數沒有執行結束,那麼它就會一直存在在內存當中,它的一些屬性也會一直存在。第二個問題是,沒有辦法實現Lodash的"真正"的佔位符,只是在遇到"_"的時候將其跳過了。

一個真正有效的柯里化函數實現起來有不少細節須要考慮,這就是Lodash存在的意義。咱們應該在理解其實現原理的前提下,享受Lodash帶來的便利。

小結

閱讀Lodash源碼真的可以瞭解不少代碼實現上的細節,Lodash在性能優化上面作了不少工做,也給咱們學習一個優秀的js庫提供了很是好的參考。我在閱讀Lodash源代碼的過程當中也會遇到不少不理解的地方。可是細細琢磨發其實它的代碼仍是很是清晰易懂的。

待續

下週將繼續更新Lodash源碼分析系列,接下來將會分析Lodash集合方法。

© 版權全部,禁止一切形式轉載。順便宣傳一下我的博客http://chenquan.me

相關文章
相關標籤/搜索