reduce探索lodash.reduce实现原理解析

目录
  • 前言
  • 基本实现
  • lodash 中的 reduce 实现有何不同?
  • 总结

前言

前一篇 你真的了解Array.reduce吗? 讲解了 reduce 基础使用方法和场景的运用场景。本篇来分析一下 reduce 函数本身的实现原理。

实现 reduce 其实挺简单的,因为它本身的运行原理也不难,就是把数组进行遍历,然后组成合适的参数传递给回调函数,只要思路对了,去尝试几次,那么就理解了 reduce 。

最具有代表性的工具库当然是 lodash,因此本篇文章的主要内容会讲解 reduce 的基本实现,以及lodash 中是怎么来实现的,做了什么处理。

基本实现

实现思路:

  • 判断是否有初始值,因为有初始值和没有初始值对回调函数(reducer)执行的次数是有影响的。
  • 遍历数组
  • 组合参数传递给 reducer 进行执行
  • 获取到第三步返回值的时候,要把返回值存储起来,在下一次便利的时候作为reducer第一个参数来替换初始值。
  • 返回最终计算的value值
function reduce(array, reducer, initialValue = null) {
    let value = initialValue === null ? array[0] : initialValue; // 思路1
    let startIndex = initialValue === null ? 1 : 0; // 思路1
    for(let i = startIndex; i < array.length; i++) { // 思路 2
        const item = array[i]
        const res = reducer(value, item, i) // 思路3
        value = res; // 思路4
    }
    return value; // 思路5
}
复制代码

测试一下:

console.log(reduce([1,2,3], (a, b) => (a + b), 0)) // 6
console.log(reduce([1,2,3], (a, b) => (a + b))) // 6
复制代码

看起来是不是挺简单的,代码其实还可以更简洁一点:

function reduce(array, reducer, value = null) {
    value = value === null ? array[0] : value;
    for(let i = null ? 1 : 0; i < array.length; i++) {
        value = reducer(value, array[i], i);
    }
    return value;
}
复制代码

lodash 中的 reduce 实现有何不同?

lodash中 的 reduce 不仅可以对数组生效,也可以对普通 object 、类数组对象生效。

不过也针对数组其实单独实现了一个 arrayReduce 函数,不过没有对外。

来看一下 reducearrayReduce 源码

function reduce(collection, iteratee, accumulator) {
  const func = Array.isArray(collection) ? arrayReduce : baseReduce
  const initAccum = arguments.length < 3
  return func(collection, iteratee, accumulator, initAccum, baseEach)
}

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

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

看得懂吗?不理解的话看下面一份代码,我把非数组类型的代码去掉,再调一下变量命名和新增注释:

function reduce(array, reducer, value) {
  const noInitialValue = arguments.length < 3 // 用参数的数量来判断是否有初始值

  let index = -1 // 遍历索引 - 1,因为下面 while 循环前先加了 1
  const length = array == null ? 0 : array.length // 判断数组是否存在和缓存数组长度
  // 这个if 语句中做了我上面思路1中初始值的问题和遍历次数的问题
  if (noInitialValue && length) { // && length 判断了数组是否为空
    value = array[++index] // 没有有初始值,则取数组中第一为,注意 index 变成了0,下面 while 循环前会先加 1,因此循环次数会少一次。
  }
  while (++index < length) {
    value = reducer(value, array[index], index, array)
  }
  return value
}
复制代码

可以看出其实大部分逻辑还是和前面的简单实现差不多,不过考虑更全一些,有值得借鉴的地方:

  • 参数判断逻辑更有力,不管外界传递了第三个参数是啥,都说明有初始值
  • 考虑了数组不存在或者为空的情况

下面我们再看一下,去除数组相关的代码来看看针对其他对象类型怎么处理的。

function reduce(collection, iteratee, accumulator) {
  const func = baseReduce;
  const initAccum = arguments.length < 3
  return func(collection, iteratee, accumulator, initAccum, baseEach)
}
复制代码

其他类型的都会教给 baseReduce 函数去处理。

// baseReduce
function baseReduce(collection, iteratee, accumulator, initAccum, eachFunc) {
  // 使用外部传递进来的遍历方法进行遍历对象,然后传递了一个 callback 给 eachFunc
  eachFunc(collection, (value, index, collection) => {
    // 初始值设置,
    accumulator = initAccum
      ? (initAccum = false, value)
      : iteratee(accumulator, value, index, collection)
  })
  return accumulator
}
复制代码

使用外部传递进来的遍历方法进行遍历对象,然后传递了一个 callback 给 eachFunc,来执行 iteratee (也就是前面说的reducer),callback 内部的代码就和前面 for 循环或者 while 循环的代码类似的,就是组合参数传递给 reducer 进行执行,不过直接看可能有点不好理解中,了解了原理再来看应该可以理解,注意事项:

  • initAccum 为 false 时,说明有初始值,直接执行 iteratee。
  • initAccum 为 true,说明没有初始值,需要添加初始值,因此第一次循环就是赋值给初始值,然后把 initAccum 设置为false,没有进行执行 iteratee,比没有初始值少一次执行,符合逻辑。

eachFunc 用的是 reduce 中传递进来的 baseEach,内部主要就是对对象属性进行遍历的操作,然后把属性值和索引以及对象本身传递给 callback,稍微需要注意的就是可能遇到类数组的对象,为了保证顺序,使用类数组放入索引进行遍历,而其他对象并不能保证属性的传递顺序,可以再看一下baseEach实现的代码:

function baseEach(collection, iteratee) {
  if (collection == null) {
    return collection
  }
  // 不是类数组则使用 baseForOwn 处理
  if (!isArrayLike(collection)) {
    return baseForOwn(collection, iteratee)
  }
  const length = collection.length
  const iterable = Object(collection) // 使用arguments测试了一下,好像没啥作用
  let index = -1

  // 遍历类数组
  while (++index < length) {
    if (iteratee(iterable[index], index, iterable) === false) {
      break
    }
  }
  return collection
}
复制代码

不是 isArrayLike 的对象遍历与本篇文章的内容没有啥关系了,因此就不深入了。

总结

最近一直在学函数式编程,而 reduce 可以很好的契合函数式编程中的函数组合思想,因此最近几篇文章中都涉及到它,就想一次性把它给写透彻,希望对读者又一些帮助。

以上就是reduce探索lodash.reduce实现原理解析的详细内容,更多关于reduce lodash.reduce实现的资料请关注我们其它相关文章!

(0)

相关推荐

  • reduce探索lodash.reduce实现原理解析

    目录 前言 基本实现 lodash 中的 reduce 实现有何不同? 总结 前言 前一篇 你真的了解Array.reduce吗? 讲解了 reduce 基础使用方法和场景的运用场景.本篇来分析一下 reduce 函数本身的实现原理. 实现 reduce 其实挺简单的,因为它本身的运行原理也不难,就是把数组进行遍历,然后组成合适的参数传递给回调函数,只要思路对了,去尝试几次,那么就理解了 reduce . 最具有代表性的工具库当然是 lodash,因此本篇文章的主要内容会讲解 reduce 的基

  • 玩转Koa之koa-router原理解析

    一.前言 Koa为了保持自身的简洁,并没有捆绑中间件.但是在实际的开发中,我们需要和形形色色的中间件打交道,本文将要分析的是经常用到的路由中间件 -- koa-router. 如果你对Koa的原理还不了解的话,可以先查看Koa原理解析. 二.koa-router概述 koa-router的源码只有两个文件:router.js和layer.js,分别对应Router对象和Layer对象. Layer对象是对单个路由的管理,其中包含的信息有路由路径(path).路由请求方法(method)和路由执行

  • Java7和Java8中的ConcurrentHashMap原理解析

    Java7 中 ConcurrentHashMap ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些. 整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表"部分"或"一段"的意思,所以很多地方都会将其描述为分段锁.注意,行文中,我很多地方用了"槽"来代表一个 segment. 简单理解就是,ConcurrentHashMap 是一个 Segm

  • java中fork-join的原理解析

    ForkJoinTask就是ForkJoinPool里面的每一个任务.他主要有两个子类:RecursiveAction和RecursiveTask.然后通过fork()方法去分配任务执行任务,通过join()方法汇总任务结果, 这就是整个过程的运用.他有两个子类,使用这两个子类都可以实现我们的任务分配和计算. (1)RecursiveAction 一个递归无结果的ForkJoinTask(没有返回值) (2)RecursiveTask 一个递归有结果的ForkJoinTask(有返回值) For

  • js中闭包结合递归等于柯里化原理解析

    引言 我们不妨以两数相加为例子,递进说明. 我们通常是这样写一个函数来求得 两数相加 的值: function sum(a,b){ console.log(a+b) } sum(1,2) 这样写一点毛病没有! 不过呢?问题总会在发展中产生,产品经理又要加一个值,需求:三数相加: 咱通常来说,第一时间,就在原基础上,直接再加一个参数就是了: 于是,修改后像是这样: function sum(a,b,c){ console.log(a+b+c) } sum(1,2,3) 问:这样写,有毛病吗?? 答

  • C++实现简单插件机制原理解析

    在我做的第一个页游项目中,服务器使用了插件的机制,但是当时的插件都是用C#写,而且如何实现的也不是很清楚.之后的几个页游项目都是自己一个人包揽服务器部分,所以一直没有写插件的需求.下一个页游项目服务器这边需要多人合作,因此我想把其他模块都独立的做成插件的模式,目前也是在探索阶段.通过网上资料查找以及自己的整理,实现了一个简单版本的插件机制.实现代码如下: 文件Object.hpp中实现了所有插件类的基类,所有插件都要继承该类. #ifndef __OBJECT_HPP__ #define __O

  • Google和Facebook不使用Docker的原理解析

    写作本文的起因是我想让修改后的分布式 PyTorch 程序能更快的在 Facebook 的集群上启动.探索过程很有趣,也展示了工业机器学习需要的知识体系. 2007 年我刚毕业后在 Google 工作过三年.当时觉得分布式操作系统 Borg 真好用. 从 2010 年离开 Google 之后就一直盼着它开源,直到 Kubernetes 的出现. Kubernetes 调度的计算单元是 containers(准确的翻译是"集装箱",而不是意思泛泛的"容器",看看 Do

  • react  Suspense工作原理解析

    目录 Suspense 基本应用 Suspense 原理 基本流程 源码解读 - primary 组件 源码解读 - 异常捕获 源码解读 - 添加 promise 回调 源码解读-Suspense 首次渲染 primary 组件加载完成前的渲染 primary 组件加载完成时的渲染 利用 Suspense 自己实现数据加载 Suspense 基本应用 Suspense 目前在 react 中一般配合 lazy 使用,当有一些组件需要动态加载(例如各种插件)时可以利用 lazy 方法来完成.其中

  • Java 并发编程:volatile的使用及其原理解析

    Java并发编程系列[未完]: •Java 并发编程:核心理论 •Java并发编程:Synchronized及其实现原理 •Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) •Java 并发编程:线程间的协作(wait/notify/sleep/yield/join) •Java 并发编程:volatile的使用及其原理 一.volatile的作用 在<Java并发编程:核心理论>一文中,我们已经提到过可见性.有序性及原子性问题,通常情况下我们可以通过Synchroniz

  • Java 8 动态类型语言Lambda表达式实现原理解析

    Java 8支持动态语言,看到了很酷的Lambda表达式,对一直以静态类型语言自居的Java,让人看到了Java虚拟机可以支持动态语言的目标. import java.util.function.Consumer; public class Lambda { public static void main(String[] args) { Consumer<String> c = s -> System.out.println(s); c.accept("hello lambd

随机推荐