前言
在刚开始计划写这些方法的时候,并没有完整的思路,也不知道该从何下手,但通过阅读和理解文档的含义,和去查看一些方法库的源码,理解他们的封装思路,渐渐开始有了一些思路
一提到封装,首先我们应该需要想到的是什么呢?
功能性:方法的主要功能是否得到实现,是否符合需求,是否可以被扩展等。
可复用性:方法能否被重复利用,是否具有独立性,是否可以被其他方法调用等。
易用性:方法是否易于使用,是否需要文档或示例来指导使用等。
性能优化:方法是否可以优化性能,例如缓存等。
(咱这就是写着玩的,条件说多了对我可能不利,其他的我也不敢再多说了)
但是我们封装出来方法能用,那这次就算是成功了,我们要怎么封装,封装之后要怎样才能使用呢?一头雾水,那咱就开始看文档吧!
首先,我们要封装的是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 | ✅ |