使用高阶函数封装属于自己ES6数组方法

前言

在刚开始计划写这些方法的时候,并没有完整的思路,也不知道该从何下手,但通过阅读和理解文档的含义,和去查看一些方法库的源码,理解他们的封装思路,渐渐开始有了一些思路

一提到封装,首先我们应该需要想到的是什么呢?

功能性:方法的主要功能是否得到实现,是否符合需求,是否可以被扩展等。

可复用性:方法能否被重复利用,是否具有独立性,是否可以被其他方法调用等。

易用性:方法是否易于使用,是否需要文档或示例来指导使用等。

性能优化:方法是否可以优化性能,例如缓存等。

(咱这就是写着玩的,条件说多了对我可能不利,其他的我也不敢再多说了)

但是我们封装出来方法能用,那这次就算是成功了,我们要怎么封装,封装之后要怎样才能使用呢?一头雾水,那咱就开始看文档吧!

首先,我们要封装的是Array的方法,那肯定需要用到Array构造函数,

其次,将方法是写在原型上,还是直接写在构造函数中呢?它们都是可以正常使用的。

其实在文档中已经明确告知我们了...

由此看出,将方法直接挂载在原型身上,实例直接调用原型身上的方法即可,但为什么还要写在原型上呢?

函数是引用数据类型,每一次通过new,都会创建一个实例对象,每一次创建的实例身上的方法的功能是一样的,但却不是同一个方法,每一个方法都会在堆内存中开辟一段新的空间,这样的话,浪费内存空间,完全没有必要,用的是同一个方法,实现的是同一个功能,那为什么不用同一个方法呢?

原型就帮我们解决了这个问题,如果将方法写在构造函数的 prototype 身上,那么他的实例就可以通过__proto__ 来找到,而且每一次找到的都是同一个。

函数天生自带的一个属性,他的作用的可以在他的身上写一些方法,以供new出来的实例使用,而我们需要用到数组的方法就是通过这个属性访问的

将我们要封装的方法直接挂载在构造函数的原型上,后面直接调用Array身上我们自己封装的方法就行 array.myXxxxx()

接下来就一起看下我们最常用的 forEach 吧,一步一步 封装属于我们自己的ES6数组方法

一、myForEach  forEach

forEach()方法是一种迭代方法。它按callbackFn索引升序为数组中的每个元素调用一次提供的函数。与 不同的是map()forEach()总是返回undefined并且不可链接。典型的用例是在链的末尾执行副作用。

1、首先将我们的方法挂载原型上

js 复制代码
 Array.prototype.myForEach = function () {
 
 }

2、在文档中,明确告知我们 forEach 方法会接受两个参数,callbackFn 和 thisArg

① 为什么forEach会有两个参数,在平时的实际开发中我们使用forEach方法时,只会传入一个参数,那另一个参数的意义是什么呢?

② 挂载在原型上的方法为什么不使用箭头函数?

在文档中也告知 thisArg 为可选参数,文档中通过对这个参数的说明,就可以解答这两个问题:

thisArg 参数(默认为 undefined)将在调用 callbackFn 时用作 this 值。最终由 callbackFn 观察到的 this 值根据通常的规则 确定:如果 callbackFn 是非严格模式(译注:正常模式/马虎模式),原始 this 值将被包装为对象,并将 undefined/null 替换为 globalThis。对于使用 箭头函数 定义的任何 callbackFn 来说,thisArg 参数都是无关紧要的,因为箭头函数没有自己的 this 绑定。

3、确定好传入的参数 那就传入我们需要的参数到我们的 myForEach 函数中,并拿到我们的this

js 复制代码
 Array.prototype.myForEach = function (callbackFn) {
   var thisArg = this
 }

4、那么在callbackFn函数里我们需要做什么操作呢,那就要根据方法的用法来确定了,这里不赘述了 文档明确告知了:

forEach() 方法是一个迭代方法。它按索引升序地为数组中的每个元素调用一次提供的 callbackFn 函数,再调用callbackFn

js 复制代码
 Array.prototype.myForEach = function (callbackFn) {
    // 首先我们会拿到方法的this,注意:forEach()方法是不会改变数组,但是callbackFn()是可以改变数组的
    var thisArg = this
    // 先声明一个变量当做数组的下标,每次执行 callbackFn时 让其下标递增,
        index = -1
    // 但当调用 forEach() 时,callbackFn 不会访问超出数组初始长度的任何元素。所以在循环 thisArg 时,当index === thisArg.length,循环就该终止了
    while (++index < thisArg.length) {
      // 在循环中,每次升序都会调用一次callbackFn 且callbackFn 是有三个参数的
      // element 数组中正在处理的当前元素
      // index 正在处理的元素的索引
      // array 调用的实例本身
      // 因为callbackFn是可以改变原数组的数据,且会返回新数组,所以在调用cb时,需要指定cb的this指向,所以我们可以调用call() 来改变cb()的this指向
      // 然后我们在依次传入所需要的三个参数
      callbackFn.call(thisArg, thisArg[index], index, thisArg)
    }

    // 最后 因为forEach 是没有返回值的 所以此处不需要 return
  }

完成封装后,我们来调用一下,看我们的封装是否成功

由此可见,myForEach封装成功,根据myForEach的思路,我们接下来封装其他几个数组

(做一点小小的说明,为了书写更规范的代码,我们在ES6的数组方法里,尽可能的不要直接去修改原数组的数据,虽然我们挂载在原型上的方法不会修改数据,但方法里的callbackFn是可以直接修改数据的,此处书写只是单纯校验方法是否封装成功)

二、myMap  map

map()方法是一种迭代方法。callbackFn它为数组中的每个元素调用一次提供的函数,并根据结果 构造一个新数组。

map 会返回一个新数组,这是一个与forEach不同的一点,依照这一点,我们照葫芦画瓢的去画就好了

map()方法是一种复制方法。它不会改变this。然而,提供的函数 ascallbackFn可以改变数组。但请注意,数组的长度是在第一次调用之前 callbackFn保存的。所以在我们使用map方法时,为了尽可能的保证代码的优雅,不要直接去修改原数组

js 复制代码
 Array.prototype.myMap = function (cb) {
   // 这里的 cb 指代上文中的 callbackFn
   var thisArg = this
       // 初始化下标
       i = -1
       // 因为map会返回一个新的数组,所以我们在这里初始化一个数组
       initArr = []
   while (++i < thisArg.length) {
     // 因为map是一种复制的方法,在cb方法里会返回根据条件对每一项进行操作后 return出来
     let result = cb.call(thisArg, thisArg[i], i, thisArg)
     // 再把返回的每一项push进初始化的数组中
     initArr.push(result)
   }
   // 在最后 返回出初始化的数组,就得到map复制的新数组了
   return initArr
 }

我们再次来检验一下,myMap是否正常

三、myFilter  filter

filter()方法是一种迭代方法cb它为数组中的每个元素调用一次提供的函数,并构造一个包含所有值的新数组,并为其cb返回值。未通过测试的数组元素cb不会包含在新数组中。

filter()map()比较相似,都会得到一个新数组

不同点在于:
map 会对实例的每一项根据cb的条件对其 进行操作 ,且 返回所有项
filter 则是cb的条件进行判断实例的每一项,如果是true,则返回,反之则不会返回,返回的值就是我们需要得到的新数组

经过前两个方法的封装,此处我们直接开始写

js 复制代码
 Array.prototype.myFilter = function (cb) {
   var thisArg = this
       i = -1
       initArr = []
   while (++i < thisArg.length) {
     let result = cb.call(thisArg, thisArg[i], i, thisArg)
     if (result) initArr.push(thisArg[i])
   }
   return initArr
 }

Checkout

四、mySome  some

some()方法是一种迭代方法。cb它为数组中的每个元素调用一次提供的函数,直到cb返回一个值。如果找到这样的元素,some()则立即返回true并停止遍历数组。否则,如果为所有元素cb返回一个some()值,则返回false

some()其作用类似于数学中的"存在"量词。特别是,对于空数组,它会false在任何条件下返回。

js 复制代码
 Array.prototype.mySome = function (cb) {
   var thisArg = this
       i = -1
       // some方法会返回根据cb方法返回一个boolean,所以在此处初始化一个布尔值
       initBoo = false
   // 此处需要增加判断条件如果存在真值,就不需要再遍历剩下的其余元素(此处可以根据初始化之后的下标或初始化布尔值去做判断)
   while (++i < thisArg.length && !initBoo) {
     let result = cb.call(thisArg, thisArg[i], i, thisArg)
     if (result) {
       // 当找到符合条件的一项,修改初始化initBoo的值为true
       initBoo = true
     }
   }
   // 最终返回初始化的initBoo
   return initBoo
 }

Checkout

五、myEvery  every

every()对数组中的每个元素执行的函数。它应该返回一个值来指示元素通过测试,否则返回一个值。

every()some()的相似度极高,我们直接将some()的条件反方向理解 即为every()的条件

every()方法是一种迭代方法。cb它为数组中的每个元素调用一次提供的函数,直到cb返回一个假值。如果找到这样的元素,some()则立即返回 false 并停止遍历数组。否则,如果为所有元素cb返回一个真值,则返回 true

但有一点与some()不同,在every()中如果cb没有给我们返回值,那every只会遍历实例的第一项,且会给出一个 undefined,所以我们需要完善对cb返回值进行的处理

js 复制代码
 Array.prototype.myEvery = function (cb) {
   var thisArg = this
       i = -1
       initBoo = undefined
   while (++i < thisArg.length) {
     let result = cb.call(thisArg, thisArg[i], i, thisArg)
     // 此处如果是 false || undefined 我们可以利用定义的下标进行判断
     if (typeof result === 'undefined') {
       i = thisArg.length
     } else if (result) {
       initBoo = true
     } else {
       initBoo = false
       i = thisArg.length
     }
   }
   return initBoo
 }

Checkout

六、myFind  find

find() 方法是一个迭代方法。它按索引升序顺序为数组中的每个元素调用提供的 cb 函数,直到 cb 返回一个真值。然后 find() 返回该元素并停止迭代数组。如果 cb 从未返回真值,则 find() 返回 undefined

js 复制代码
 Array.prototype.myFind = function (cb) {
   var thisArg = this
       i = -1
       initObj = undefined
   while (++i < thisArg.length) {
     let result = cb.call(thisArg, thisArg[i], i, thisArg)
     if (result) {
       initObj = thisArg[i]
       i = thisArg.length
     }
   }
   return initObj
 }

Checkout

七、myFindIndex  findIndex

findIndex()是一种迭代方法。它cb按升序索引顺序为数组中的每个元素调用一次提供的函数,直到cb返回值。findIndex()然后返回该元素的索引并停止遍历数组。如果cb从不返回真值,findIndex()则返回-1

js 复制代码
 Array.prototype.myFindIndex = function (cb) {
   var thisArg = this
       i = -1
       initIndex = -1
   while (++i < thisArg.length) {
     let result = cb.call(thisArg, thisArg[i], i, thisArg)
     if (result) {
       initIndex = i
       i = thisArg.length
     }
   }
   return initIndex
 }

Checkout

八、myReduce  reduce

reduce() 实例的方法 按Array顺序对数组的每个元素执行用户提供的"reducer"回调函数,并传入前一个元素计算的返回值。对数组的所有元素运行缩减程序的最终结果是单个值。

第一次运行回调时,没有"先前计算的返回值"。如果提供的话,可以使用初始值来代替它。否则,索引 0 处的数组元素将用作初始值,并且迭代从下一个元素开始(索引 1 而不是索引 0)

js 复制代码
 Array.prototype.myReduce = function (cb, initVal = undefined) {
   var thisArg = this
       i = -1
   // 根据文档,reduce方法会传入两个参数,第二个参数为可选参数,
   // 如果未传参,则取数组的第一项为cb的acc,且从下标为1的项开始遍历
   // 如果传入了 initValue,则正常的从第一项开始遍历
   if (typeof initVal === 'undefined') {
     i = 0
     initVal = thisArg[0]
   }
   while (++i < thisArg.length) {
     initVal = cb.call(thisArg, initVal, thisArg[i], i, thisArg)
   }
   return initVal
 }

Checkout

1、累计

2、在平常的开发中,也会时常用到reduce来做数组去重

写在最后

在我们日常的开发过程中,以上八种数组的方法的使用频率是相对较高的,但很多时候为了更快的完成需求,并没有合理的去使用更适合的方法,这也会导致增加项目维护复杂度

简单的说,使用不同的方法,在对某些数据处理可以提升不错的效率,但对于目前的电脑来说,数据量没有达到一定的程度,这点效率消耗用户是感觉不到的,所以导致在前期开发过程中,随便使用能完成需求的方法也就不了了之,而没有去选择更适合的方法

比如:

1、map 的效率一定是比 forEach 差的,因为它还要创建新数组,往新数组里插数据。

2、完全/非完全遍历

数组的遍历方法,是指这个方法接收一个回调函数作为参数,这个回调函数 起码 会接收到当前值、当前位置和数组本身作为参数。在本不需要完全遍历就能获取到想要的结果的时候,你在项目中却做了完全遍历,这完全是没有必要的,而有的遍历需要根据条件跳出循环,有的方法不需要语法层面的控制就能结束循环

因此,遍历方法可以分为「完全遍历」和「非完全遍历」两种。

方法 完全遍历
forEach
filter
map
some
every
find
findLast
findIndex
reduce
相关推荐
jump68023 分钟前
axios
前端
spionbo26 分钟前
前端解构赋值避坑指南基础到高阶深度解析技巧
前端
用户40993225021230 分钟前
Vue响应式声明的API差异、底层原理与常见陷阱你都搞懂了吗
前端·ai编程·trae
开发者小天32 分钟前
React中的componentWillUnmount 使用
前端·javascript·vue.js·react.js
永远的个初学者1 小时前
图片优化 上传图片压缩 npm包支持vue(react)框架开源插件 支持在线与本地
前端·vue.js·react.js
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ1 小时前
npm i / npm install 卡死不动解决方法
前端·npm·node.js
Kratzdisteln1 小时前
【Cursor _RubicsCube Diary 1】Node.js;npm;Vite
前端·npm·node.js
杰克尼1 小时前
vue_day04
前端·javascript·vue.js
明远湖之鱼2 小时前
浅入理解跨端渲染:从零实现 React DSL 跨端渲染机制
前端·react native·react.js
悟忧3 小时前
规避ProseMirror React渲染差异带来的BUG
前端