资讯专栏INFORMATION COLUMN

Lodash 源码分析(二)“Function” Methods

cheukyin / 2086人阅读

摘要:众所周知,函数能够将一个集合进行折叠。我们看到源代码是这样的在官方的注释中说,对于对象,遍历顺序是无法保证的。我在阅读源代码的过程中也会遇到很多不理解的地方。待续下周将继续更新源码分析系列,接下来将会分析集合方法。

前言

这是Lodash源码分析的第二篇文章,我们在第一篇Lodash 源码分析(一)“Function” Methods中介绍了基本的_.after_.map,以及复杂的_.ary函数的实现以及我们自己的自定义轻量级版本。大概清楚了Lodash的整个代码脉络。这次我们继续分析,这次我们讲讲_.reduce_.curry

_.reduce

我一直觉得,如果能够理解_.map_.reduce的实现,那么任何复杂的函数都不在话下。我们已经介绍了_.map的实现,知道了_.map函数中是如何处理集合,并将其逐个进行函数处理的。我们知道在_.map函数中会把三个参数传到给定的函数中,分别是array[index]indexarray。这次我们看看_.reduce函数。

众所周知,_.reduce函数能够将一个集合进行"折叠"。"折叠"理解起来比较抽象。我们可以通过代码作为样例说明一下:

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

如果你不知道_.reduce到底是怎么工作的,那么你可以看看我写的这篇文章从Haskell、JavaScript、Go看函数式编程。我们今天的目的是看看lodash是如何实现_.reduce的,以及和我们函数式的实现的区别。

我们看到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,那么返回值就会使8

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/85011.html

相关文章

  • Lodash 源码分析(三)Array

    摘要:前言这是源码分析系列文章的第三篇,前面两篇文章源码分析一源码分析二分别分析了中的一些重要函数,也给出了简化的实现,为理解其内部机理和执行方式提供了便利。官方也对其进行了说明。 前言 这是Lodash源码分析系列文章的第三篇,前面两篇文章(Lodash 源码分析(一)Function Methods、Lodash 源码分析(二)Function Methods)分别分析了Lodash F...

    ZoomQuiet 评论0 收藏0
  • lodash源码分析之数组的差集

    摘要:依赖源码分析之缓存使用方式的进一步封装源码分析之源码分析之源码分析之的实现源码分析之源码分析的调用如果有传递,则先调用,使用生成要比较数组的映射数组。循环完毕,没有在第二个数组中发现相同的项时,将该项存入数组中。 外部世界那些破旧与贫困的样子,可以使我内心世界得到平衡。——卡尔维诺《烟云》 本文为读 lodash 源码的第十七篇,后续文章会更新到这个仓库中,欢迎 star:pocke...

    Noodles 评论0 收藏0
  • lodash源码分析之自减的两种形式

    摘要:作用与用法是的内部函数,之前在源码分析之缓存介绍过一种这样的数据结构这是一个二维数组,每项中的第一项作为缓存对象的,第二项为缓存的值。 这个世界需要一个特定的恶人,可以供人们指名道姓,千夫所指:全都怪你。——村上春树《当我谈跑步时我谈些什么》 本文为读 lodash 源码的第六篇,后续文章会更新到这个仓库中,欢迎 star:pocket-lodash gitbook也会同步仓库的更新...

    Keven 评论0 收藏0
  • lodash源码分析之compact中的遍历

    摘要:到这里,源码分析完了。但是,有两个致命的特性的遍历不能保证顺序会遍历所有可枚举属性,包括继承的属性。的遍历顺序依赖于执行环境,不同执行环境的实现方式可能会不一样。 小时候,乡愁是一枚小小的邮票, 我在这头, 母亲在那头。 长大后,乡愁是一张窄窄的船票, 我在这头, 新娘在那头。 后来啊, 乡愁是一方矮矮的坟墓, 我在外头, 母亲在里头。 而现在, 乡愁是一湾浅浅的海峡, 我在这头, 大...

    dmlllll 评论0 收藏0
  • 源码分析】给你几个闹钟,或许用 10 分钟就能写出 lodash 中的 debounce &

    摘要:最简单的案例以最简单的情景为例在某一时刻点只调用一次函数,那么将在时间后才会真正触发函数。后续我们会逐渐增加黑色闹钟出现的复杂度,不断去分析红色闹钟的位置。 序 相比网上教程中的 debounce 函数,lodash 中的 debounce 功能更为强大,相应的理解起来更为复杂; 解读源码一般都是直接拿官方源码来解读,不过这次我们采用另外的方式:从最简单的场景开始写代码,然后慢慢往源码...

    余学文 评论0 收藏0

发表评论

0条评论

cheukyin

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<