使用高阶函数封装属于自己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
相关推荐
小满zs16 分钟前
React-router v7 第四章(路由传参)
前端·react.js
小陈同学呦25 分钟前
聊聊双列瀑布流
前端·javascript·面试
键指江湖1 小时前
React 在组件间共享状态
前端·javascript·react.js
诸葛亮的芭蕉扇1 小时前
D3路网图技术文档
前端·javascript·vue.js·microsoft
小离a_a1 小时前
小程序css实现容器内 数据滚动 无缝衔接 点击暂停
前端·css·小程序
徐小夕2 小时前
花了2个月时间研究了市面上的4款开源表格组件,崩溃了,决定自己写一款
前端·javascript·react.js
by————组态2 小时前
低代码 Web 组态
前端·人工智能·物联网·低代码·数学建模·组态
拉不动的猪2 小时前
UniApp金融理财产品项目简单介绍
前端·javascript·面试
菜冬眠。2 小时前
uni-app/微信小程序接入腾讯位置服务地图选点插件
前端·微信小程序·uni-app
jayson.h2 小时前
pdf解密程序
java·前端·pdf