一次性手写掉js中所有遍历数组的方法,建议收藏!!!

852 阅读26分钟

今天我们来手写一下js中遍历数组的方法,让你彻底搞懂这些方法的原理,我们再来使用它们时就得心应手了。建议收藏,反复阅读!字数写的最多的一次,整理不易,求个免费的赞!

1. forEach

首先第一个,我们来写一下forEach。既然要手写一个forEach,那就肯定得知道它是怎么用的,它有哪些特点。所以我们先来认识一下它。

let arr = ['a', 'b', 'c', 'd', 'e']

arr.forEach((item, index, arr) => {
    console.log(item, index, arr);
})

我们有一个数组arr,我们使用forEach去遍历它。forEach会接受一个回调函数,回调函数接受3个参数,第一个参数item代表数组中的每一项,第二个参数代表数组的下标,第三个参数代表原数组。我们输出3个参数看一下。此时输出结果会是:

image.png

合情合理,一点毛病没有。我们发现输出了5个结果,那你说forEach的源代码中是不是一定有一个for循环遍历了整个数组,不然它是怎么输出5次的呢。所以forEach其实就是把for循环封装了一下给我们使用,然后在for循环的基础上增加了一些功能。

那forEach有返回值吗?我们来试一下:

let arr = ['a', 'b', 'c', 'd', 'e']

const res = arr.forEach((item, index, arr) => {
    console.log(item, index, arr);
})
console.log(res);

我们定义一个res为forEach的执行结果,输出看一下是否有值。

image.png

我们发现res是undefined,说明forEach是没有返回结果的,好,我们记住了。

forEach函数差不多就这几个特性,接下来,我们可以手写一个自己的forEach函数。

首先第一点,我们想让数组的实例对象能使用这个方法,那这个方法一定是写在构造函数Array的原型上的,所以我们在Array的原型上定义一个方法my_forEach,就作为我们自己的forEach方法。

let arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_forEach = function (cb) {
   
}

forEach函数是会接收一个回调函数的,所以我们也在我们的my_forEach函数上接收一个回调函数,就叫cb。

接下来是不是应该用for循环去遍历arr数组啊,那我们能在my_forEach函数里直接写arr数组吗?那要是我们用过一个数组去调用这个my_forEach方法呢,那不就出问题了。那应该怎么拿到这个数组呢?你说将来这个my_forEach方法一定是会被某个数组调用的吧,那my_forEach中的this是不是就会指向调用它的数组啊,触发隐式绑定嘛。所以我们直接在my_forEach中遍历this就行了,它代表的就是调用它的数组。

所以接下来我们这样写:

let arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_forEach = function (cb) {
    for (let i = 0; i < this.length; i++) {
    
    }
}

这样就会遍历这个数组中的每一项。那我们在循环里去做什么呢?那是不是就是去调用这个回调函数吧,将三个实参传给它,那传哪三个实参呢?我们定义这个回调函数的时候让它接收了3个形参,item、index和arr,分别代表什么你已经清楚了。所以我们只要把 this[ i ] , i 和 this 作为实参传给这个回调函数cb就行了。

let arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_forEach = function (cb) {
    for (let i = 0; i < this.length; i++) {
        cb(this[i], i, this);
    }
}

而因为原本的forEach函数就没有返回结果,所以我们的my_forEach也不写返回值。这样就写完了,还是比较简单的吧。现在我们来看看效果是否和原本的一样:

let arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_forEach = function (cb) {
    for (let i = 0; i < this.length; i++) {
        cb(this[i], i, this);
    }
}

const res = arr.my_forEach((item, index, arr) => {
    console.log(item, index, arr);
})
console.log(res);

image.png

有5条输出结果并且函数最后没有返回值,和原本的效果一模一样。这样我们就手写了一个forEach出来。

2. map

写完了forEach,我们再来写一下map。

那同样的,我们想要手写一个map,那就得认识清楚一下map。

map和forEach一样,也会接收一个回调函数,回调函数会接受三个形参item、index和arr。

let arr = ['a', 'b', 'c', 'd', 'e']

arr.map((item, index, arr) => {
    console.log(item, index, arr);
})

分别代表什么你已经知道了:

image.png

那它与forEach的不同之处是什么呢?map的回调函数要求我们写return,它允许我们对item进行一些操作,而且map是有返回结果的,它会返回一个新的数组,新的数组里存放的就是我们操作过的item。

let arr = ['a', 'b', 'c', 'd', 'e']

const res = arr.map((item, index, arr) => {
    return item + '1'
})
console.log(res, arr);

我们定义一个res为map的执行结果,在map的回调函数里,我们将item拼接一个字符串1,然后返回item。我们把res和arr都输出看一下,看看它是否会影响原数组。

image.png

得到了一个新的数组res,新数组中的每一项都是arr中每一项拼接了字符串1,并且原数组arr没有受影响。

所以map身上有这样一些特性。接下来我们就能来手写自己的map方法了,我们就叫它my_map吧。

my_map同样要写在Array的原型上,已经不需要再过多解释了,并且它也要接受一个回调函数cb。

let arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_map = function (cb) {
    
}

在my_map里面我们同样要去进行for循环,在每次循环我们调用cb函数,给它传3个实参,和forEach时一样。

let arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_map = function (cb) {
    for (let i = 0; i < this.length; i++) {
        cb(this[i], i, this)
    }
}

而和forEach不一样的是,现在的回调函数是有返回结果的,它会返回我们操作过的item,并且my_map函数也是有返回结果的,它会返回一个新数组,新数组里存放的就是新item。所以我们要提前准备一个空数组,在每次循环时将cb的返回结果push到新数组里去,循环结束后返回这个新数组。

所以我们这样写:

let arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_map = function (cb) {
    let result = []
    for (let i = 0; i < this.length; i++) {
        result.push(cb(this[i], i, this))
    }
    return result
}

这样,我们就手写完了自己的map方法,我们来测试一下看看是否和原函数一样:

let arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_map = function (cb) {
    let result = []
    for (let i = 0; i < this.length; i++) {
        result.push(cb(this[i], i, this))
    }
    return result
}

const res = arr.my_map((item, index) => {
    return item + '1'
})
console.log(res, arr);

image.png

没有毛病,输出结果一样。

3. filter

继续。下一个。我们来手写一个filter。

filter函数又有些什么样的特性呢?filter翻译过来应该是过滤的意思吧,所以它可以遍历数组把满足条件的item筛选出来。

它也会接受一个回调函数,回调函数接受3个形参,并且它的回调函数也要求我们写return,我们可以return 出来满足某个条件的item,并且filter函数也会有返回结果,它的返回结果也是一个新数组,新数组里放的就是满足条件的item。

let arr = [1, 2, 3, 4, 5, 6]

const res = arr.filter((item, index, arr) => {
    return item > 3
})
console.log(res, arr);

我们在回调函数里return item > 3,看看它是否会把大于3的item筛选出来并存放到一个新数组里。我们同样输出一下res和arr。

image.png

看,res是一个新数组并且它的每一项都是大于3的,而原数组arr也没有受影响。

那我们来写一个自己的my_filter方法,你说它是不是和map函数不要太相似啊,map函数是将回调函数的返回结果push到一个新数组里,而在filter中,回调函数的执行结果会是什么?会是true or false吧。会发生隐式转换的,如果item小于3就是false,如果大于3就是true。所以我们可以利用这一点去做判断。如果条件为true,就将这一个item添加到新数组里去。

let arr = [1, 2, 3, 4, 5, 6]

Array.prototype.my_filter = function (cb) {
    let newArr = []
    for (var i = 0; i < this.length; i++) {
        if (cb(this[i], i, this)) {
            newArr.push(this[i])
        }
    }
    return newArr
}

我们在for循环里增加一条if语句。并且同样去调用回调函数,回调函数cb(this[i], i, this)的执行结果是一个布尔类型,我们直接将它作为if语句的判断条件,如果是true说明是符合条件的,就将this[i]添加的新数组中,最后返回这个新数组。

这样我们就写完了my_filter函数,也是比较好理解的吧。我们也来测一下我们的my_filter函数。

let arr = [1, 2, 3, 4, 5, 6]

Array.prototype.my_filter = function (cb) {
    let newArr = []
    for (var i = 0; i < this.length; i++) {
        if (cb(this[i], i, this)) {
            newArr.push(this[i])
        }
    }
    return newArr
}

const res = arr.my_filter((item, index, arr) => {
    return item > 3
})
console.log(res, arr);

我们去把大于3的item筛选出来:

屏幕截图 2024-12-25 094800.png

和map函数一样没毛病。

4. every

继续,下一个,我们再来手写一个erery函数。

那erery函数有些什么特性呢?它翻译过来是‘每一个’的意思吧,所以它是可以判断数组中的每一项是否都满足一个条件,它也接受一个回调函数,回调函数接收3个参数,回调函数也要求我们写return,并且erery函数也是有返回结果的。

我们来看一下:

let arr = [1, 2, 3, 4, 5, 6]

const res = arr.every((item, index, arr) => {
    return item > 2
})
console.log(res);

我们return item > 2,意思就是看看数组中的每一项是否都大于2,那我们已经知道了,数组的第一项就小于2,所以它的输出结果是false。

image.png

那你说这个every函数中的for循环会不会每一项都遍历呢?不需要吧,只要找到了一个不满足条件的输出结果就是false了,那就不需要接着遍历了,所以every函数中的for循环执行次数不固定,是根据回调函数的结果决定的。

了解完了这些,我们就来写我们的my_every函数。这个函数虽然有返回结果但是是布尔类型,所以就不需要准备一个空数组了。

let arr = [1, 2, 3, 4, 5, 6]

Array.prototype.my_every = function (cb) {
    for (let i = 0; i < this.length; i++) {
        
    }
}

那我们在for循环中也要去调用cb函数,cb函数的返回结果要么是true要么是false,我们也可以利用这一点,只要cb函数的执行结果出现了false,说明就有一项不满足条件,我们就直接返回false,不继续遍历了;如果for循环结束了cb函数的执行结果都是true,说明数组中的每一项都符合条件,我们就返回true。

let arr = [1, 2, 3, 4, 5, 6]

Array.prototype.my_every = function (cb) {
    for (let i = 0; i < this.length; i++) {
        if (!cb(this[i], i, this)) {
            return false;
        }
    }
    return true;
}

这样就写完了,我们也来测一下:

let arr = [1, 2, 3, 4, 5, 6]

Array.prototype.my_every = function (cb) {
    for (let i = 0; i < this.length; i++) {
        if (!cb(this[i], i, this)) {
            return false;
        }
    }
    return true;
}
const res = arr.my_every((item, index, arr) => {
    return item > 0
})
console.log(res);

我们看看数组中的每一项是否都大于0,输出结果应该是true:

image.png

我们再来看看每一项是否都大于2:

let arr = [1, 2, 3, 4, 5, 6]

Array.prototype.my_every = function (cb) {
    for (let i = 0; i < this.length; i++) {
        if (!cb(this[i], i, this)) {
            return false;
        }
    }
    return true;
}
const res = arr.my_every((item, index, arr) => {
    return item > 2
})
console.log(res);

image.png

输出结果是false没错。

5. some

写完了every当然要来写它的兄弟some了,一个是every一个是some,应该猜都能猜到它的作用是什么吧。ervey函数是判断数组中每一项是否都满足条件,那some函数就是判断数组中是否有一项满足条件,只要有一项满足条件我就是true。

let arr = [1, 2, 3, 4, 5, 6]

const res = arr.some((item, index, arr) => {
    return item > 3
})
console.log(res);

我们return item > 3,输出结果应该是什么呀?是true吧,只要数组中有大于3的就是true,除非你每一项都不满足条件。

image.png

所以它的代码简直和every一模一样吧,只要改一下判断条件,只要cb函数的执行结果出现了true,说明就有一项满足条件,直接返回true,就不用接着遍历了。

let arr = [1, 2, 3, 4, 5, 6]

Array.prototype.my_some = function (cb) {
    for (let i = 0; i < this.length; i++) {
        if (cb(this[i], i, this)) {
            return true;
        }
    }
    return false;
}

我们也来测一下:

let arr = [1, 2, 3, 4, 5, 6]

Array.prototype.my_some = function (cb) {
    for (let i = 0; i < this.length; i++) {
        if (cb(this[i], i, this)) {
            return true;
        }
    }
    return false;
}

const res = arr.my_some((item, index, arr) => {
    return item > 3
})
console.log(res);

我们来判断一下数组中是否有大于3的,输出结果应该是true:

image.png

let arr = [1, 2, 3, 4, 5, 6]

Array.prototype.my_some = function (cb) {
    for (let i = 0; i < this.length; i++) {
        if (cb(this[i], i, this)) {
            return true;
        }
    }
    return false;
}

const res = arr.my_some((item, index, arr) => {
    return item > 6
})
console.log(res);

再来判断一下数组中是否有大于6的,输出结果应该是false:

image.png

6. reduce

我们再来手写一下reduce,这个函数就比较特殊了,相对前面几个也比较复杂,我们一起来看一下。

首先,我们来看一下它是怎么用的,再来搞清楚它身上的一些特性,动起手来才会比较好写。

let arr = [1, 2, 3, 4, 5, 6]

arr.reduce((pre, item, index, arr) => {
    console.log(pre, item, index, arr);
}, 0)

reduce函数它和前面几个不一样的是,它除了能接收一个回调函数外,它还能接收第二个参数,我们就随便写个0。并且它接收的回调函数中有4个参数,多了一个pre。这个pre是什么呢?pre的初始值会被赋值为你传进来的第二个参数。我们输出这四个参数看一下:

image.png

你看,第一条输出结果的pre是0吧,然后是item、index和arr。那为什么从第二条输出结果开始pre的值就变成undefined了呢?这是因为我们在回调函数里没有写return,reduce函数的回调函数也要求我们写return,并且return得到的结果会重新赋值给pre。

let arr = [1, 2, 3, 4, 5, 6]

arr.reduce((pre, item, index, arr) => {
    console.log(pre, item, index, arr);
    return pre + item;
}, 0)

我们return pre + item,再输出这四个参数看看:

image.png

你看,每一条输出结果的pre都有值,并且因为我们return的是pre + item,所以pre的值是上一次pre和item的和。所以当我们没有写return时,回调函数的执行结果就是undefined,又因为回调函数的执行结果会重新赋给pre,所以之后pre就变成undefined了。

并且reduce函数也是有返回结果的,我们可以输出一下看看:

let arr = [1, 2, 3, 4, 5, 6]

const res = arr.reduce((pre, item, index, arr) => {
    console.log(pre, item, index, arr);
    return pre + item;
}, 0)
console.log(res);

image.png

我们发现res的值是21,就是最后一次循环结束后pre的值吧,你看最后一条输出结果,pre是15,item是6,相加就是21,在最后一次循环重新赋给pre,所以最后pre的值是21,reduce函数的执行结果就是pre。

所以回调函数中 return 的值赋会给pre, 当数组遍历结束,返回最终的pre的值

那要是我们不传第二个参数呢?

let arr = [1, 2, 3, 4, 5, 6]

arr.reduce((pre, item, index, arr) => {
    console.log(pre, item, index, arr);
    return pre + item;
})

我们没有传第二个参数,再输出这四个参数看看:

image.png

我们发现就只有5条输出结果了,并且你看第一条输出结果,pre的值为1,item的值为2。这说明什么?说明当我们没有传第二个参数时,pre的初始值被赋值为数组的第一个值了,然后少执行了一次循环,从数组的第二项开始循环了。ok,我们记下了,当时候写我们自己的reduce函数时得考虑这一点。

我们再来多探讨一下reduce函数的特性,你说当我们传一个undefined作为reduce函数的第二个参数时,它是会被当作传了一个参数呢,还是当作没传参数呢?如果它当作传了一个参数,那输出结果就会有6条;如果当作没传,那输出结果就会是5条。我们来试一下:

let arr = [1, 2, 3, 4, 5, 6]

arr.reduce((pre, item, index, arr) => {
    console.log(pre, item, index, arr);
    return pre + item;
}, undefined)

image.png

我们发现,输出结果是6条,说明当我们传一个undefined时,它也是被当作传了一个参数的,pre的初始值就是undefined了,所以第一条输出结果pre是undefined。那之后pre的值怎么变成NaN了呀,我们简单分析一下就能知道。

当我们传一个undefined时,pre的初始值就是undefined,所以第一条输出结果中的pre就是undefined。之后因为我们return pre + item,那undefined加上一个数字会是什么呢?会发生隐式转换吧,undefined会被转换成NaN,那NaN加一个数字自然就是NaN了,所以这个NaN又被重新赋给pre,所以之后pre的值就都是NaN了。

reduce函数身上的特性差不多就这些,有许多我们需要考虑的点。接下来,我们就来手写一个自己的my_reduce。

我们先来考虑当我们传了一个参数并且不是undefined时应该怎么写:

let arr = [1, 2, 3, 4, 5, 6]

Array.prototype.my_reduce = function (cb, start) {
    for (let i = 0; i < this.length; i++) {
    
    }
}

my_reduce接收两个参数,一个回调函数,一个参数我们就叫start。然后去进行for循环,在for循环里我们去调用回调函数,给它传四个形参,start, this[i], i, this,这样pre的初始值就是start,没问题。

let arr = [1, 2, 3, 4, 5, 6]

Array.prototype.my_reduce = function (cb, start) {
    for (let i = 0; i < this.length; i++) {
        cb(start, this[i], i, this)
    }
}

然后回调函数是有返回结果的,这个返回结果会被重新赋给pre,所以我们将回调函数的返回结果赋给start,这样start的值就被更新了,start代表的就是pre嘛。最后循环结束我们返回start。

let arr = [1, 2, 3, 4, 5, 6]

Array.prototype.my_reduce = function (cb, start) {
    for (let i = 0; i < this.length; i++) {
        start = cb(start, this[i], i, this);
    }
    return start;
}

这是当我们传了一个参数时,那要是我们没传参数呢?我们是不是就去判断start是否为undefined吧,但会有个问题,那要是我传的就是undefined呢?如果我们去判断start是否为undefined来判断传没传值,那当我真的传一个undefined的话,就会漏掉这种情况吧。

那应该怎么办呢?当我们没传值时不能使用undefined来判断,那怎么判断呢?你还记不记得这样一个东西:...args。它叫rest参数,当我们把它写在函数的括号里时,它会帮我们收集函数剩余的参数存放到args数组里。那如果我们没传参数,args是不是就是一个空数组;如果我们传了一个undefined,这个args数组里也会帮我们放一个undefined,args就不是空数组了。所以我们可以以args是否为空数组去判断是否传了一个值。

所以我们这样写:

let arr = [1, 2, 3, 4, 5, 6]

Array.prototype.my_reduce = function (cb, ...args) {
    let start
    if (args.length) {
        start = args[0]
    } else {
        start = this[0];
    }
    for (let i = 0; i < this.length; i++) {
        start = cb(start, this[i], i, this);
    }
    return start;
}

我们第二个参数放上 ...args,然后去判断args的length,如果有长度,说明传了一个值,就将args[0] 赋给start;如果没有长度,说明没有传值,就将this[0] 赋给start。

这样就写完了吗?还记不记得当我们没传参数时,循环只执行了5次,会从数组的第二项开始循环。所以我们还要准备一个index初始值为0,当我们没有传参数时,就将index改为1,然后让 i = index 就行了。

let arr = [1, 2, 3, 4, 5, 6]

Array.prototype.my_reduce = function (cb, ...args) {
    let start, index = 0
    if (args.length) {
        start = args[0]
    } else {
        start = this[0];
        index = 1
    }
    for (let i = index; i < this.length; i++) {
        start = cb(start, this[i], i, this);
    }
    return start;
}

这样当我们没有传参数时,循环就会从第二项开始。

这样这个my_reduce函数我们就写完了,而且我们还考虑了各种情况。各位可以自行去测试一下,应该没有什么问题,在这里我就不去写测试结果了。

7. findIndex

我们继续下一个,findIndex。你看着它的字面意思猜一下它的作用,就是找下标吧。

所以它的作用很简单,就是去找一个数组里是否存在这个值,找到了就返回它的下标,没找到就返回-1。我们来看一下:

let arr = ['a', 'b', 'c', 'd', 'e']

const res = arr.findIndex((item, i, arr) => {
    return item === 'd'
})
console.log(res);

它也要求我们在回调函数里写return,并且findIndex有返回结果,你已经知道了返回结果会是什么,是我们要找到元素的下标。所以在这里我们找一下 'd' 。arr中是存在 'd' 的,所以它应该输出3。

image.png

没问题。我们再去找一下 ‘f’ 。arr中没有 ‘f’ ,所以它会返回-1。

let arr = ['a', 'b', 'c', 'd', 'e']

const res = arr.findIndex((item, i, arr) => {
    return item === 'f'
})
console.log(res);

image.png

返回-1没问题。我们来写一个自己的my_findIndex。应该很好写吧,去循环遍历数组,找到了就返回下标就行了。回调函数的返回结果会是一个布尔类型,所以我们将它作为if的判断条件。如果为true,就返回下标;如果循环结束了if都没有走进了就返回-1。

let arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_findIndex = function (cb) {
    for (let i = 0; i < this.length; i++) {
        if (cb(this[i], i, this)) {
            return i
        }
    }
    return -1
}

这样就行了。我们来测试一下:

let arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_findIndex = function (cb) {
    for (let i = 0; i < this.length; i++) {
        if (cb(this[i], i, this)) {
            return i
        }
    }
    return -1
}

const res = arr.my_findIndex((item, i, arr) => {
    return item === 'a'
})
console.log(res);

我们去找 'a' ,应该返回0。

image.png

let arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_findIndex = function (cb) {
    for (let i = 0; i < this.length; i++) {
        if (cb(this[i], i, this)) {
            return i
        }
    }
    return -1
}

const res = arr.my_findIndex((item, i, arr) => {
    return item === 'f'
})
console.log(res);

我们去找 'f' 。应该返回-1。

image.png

8. find

我们再来手写findIndex的兄弟 find,它和findIndex效果一样,都是去找数组中的某一个值。findIndex是返回下标,find就是返回这个要找的值。

let arr = ['a', 'b', 'c', 'd', 'e']

const res = arr.find((item, i, arr) => {
    return item === 'd'
})
console.log(res);

我们找 'd':

image.png

返回d。

我们要找'f':

image.png

直接返回undefined。

那这简直不要太好写吧,直接把my_findIndex的代码复制过来改一下就好了:

let arr = ['a', 'b', 'c', 'd', 'e']
Array.prototype.my_find = function (cb) {
    for (let i = 0; i < this.length; i++) {
        if (cb(this[i], i, this)) {
            return this[i];
        }
    }
    return undefined;
}

当if走进来了返回this[i]。循环结束都没有走进来返回undefined。很简单,各位可以自行测试一下。

9. includes

我们继续下一个includes。

includes是怎么用的呢?它就和前面几个都不一样了。它不接收一个回调,直接接收一个参数。它也是去判断数组中是否存在一个值,如果存在返回true,否则返回false。

let arr = ['a', 'b', 'c', 'd', 'e']

const res = arr.my_includes('a')

console.log(res);

我们去找'a':

image.png

返回true。

let arr = ['a', 'b', 'c', 'd', 'e']

const res = arr.my_includes('f')

console.log(res);

我们去找'f':

image.png

返回false。

它还能接收第二个参数,第二个参数代表要从哪个下标开始找:

let arr = ['a', 'b', 'c', 'd', 'e']

const res = arr.includes('a', 0)

console.log(res);

我们从下标0开始找,应该返回true:

屏幕截图 2024-12-25 132656.png

let arr = ['a', 'b', 'c', 'd', 'e']

const res = arr.includes('a', 1)

console.log(res);

我们从下标1开始找,应该返回false:

屏幕截图 2024-12-25 132722.png

那要是我们传一个字符串0呢?

let arr = ['a', 'b', 'c', 'd', 'e']

const res = arr.includes('a', '0')

console.log(res);

屏幕截图 2024-12-25 132656.png

也是true,说明它会将第二个参数转换为数字类型。那要是传一个字符串'hello'呢?

let arr = ['a', 'b', 'c', 'd', 'e']

const res = arr.includes('a', 'hello')

console.log(res);

屏幕截图 2024-12-25 132656.png

它也是true,'hello'转换成数字是NaN吧,那传一个NaN呢?

let arr = ['a', 'b', 'c', 'd', 'e']

const res = arr.includes('a', NaN)

console.log(res);

屏幕截图 2024-12-25 132656.png

它也是true。说明当第二个参数没传或者转换成数字类型是NaN的话,它就默认从0开始找;当第二个参数传了一个数字或者转换成数字类型后是一个数字,就从这个数字下标开始找。

所以我们动手来写自己的my_includes。

这里我们也不能通过undefined去判断第二个参数传没传,所以也要用...args收集第二个参数。

所以我们这样写:

let arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_includes = function (item, ...args) {
    for (let i = args[0]; i < this.length; i++) {
        if (this[i] === item) return true;
    }
    return false
}

当第二个参数传的是一个数字时,我们可以直接让i = args[0],然后去循环。但这样写显然考虑不够,如果没传或者传了一个字符串呢?所以我们要对args[0] 进行类型判断:

let arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_includes = function (item, ...args) {
    let index
    if (args.length && !Number.isNaN(Number(args[0]))) {
        index = Number(args[0])
    } else {
        index = 0;
    }
    for (let i = index; i < this.length; i++) {
        if (this[i] === item) return true;
    }
    return false
}

我们将args[0] 先转换成数字类型,然后去判断它是不是NaN,如果不是NaN且数组长度存在,就说明传了第二个参数,且第二个参数要么就是数字要么转换成数字后是数字,我们就让index = Number(args[0]);否则就当作没传,让index = 0。然后让i = index就行了。

这样就写完了,我们可以来测试一下:

let arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_includes = function (item, ...args) {
    let index
    if (args.length && !Number.isNaN(Number(args[0]))) {
        index = Number(args[0])
    } else {
        index = 0;
    }
    for (let i = index; i < this.length; i++) {
        if (this[i] === item) return true;
    }
    return false
}

const res = arr.my_includes('a', '0')

console.log(res);

屏幕截图 2024-12-25 132656.png

返回true,没问题。

let arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_includes = function (item, ...args) {
    let index
    if (args.length && !Number.isNaN(Number(args[0]))) {
        index = Number(args[0])
    } else {
        index = 0;
    }
    for (let i = index; i < this.length; i++) {
        if (this[i] === item) return true;
    }
    return false
}

const res = arr.my_includes('a', 'hello')

console.log(res);

屏幕截图 2024-12-25 132656.png

也返回true,因为'hello'被转换成数字是NaN,而NaN默认当作没传。没传就从下标0开始找,所以是true。

这样我们就写完了我们的my_includes方法。

10. splice

终于来到我们今天的最后一个函数了,splice。作为压轴出场,它是我们今天这些函数中最复杂的一个,我们一起来看一下。

splice函数既可以删除数组中的元素也可以增加一个元素到数组中。

当一个参数都不传时:

let arr = ['a', 'b', 'c', 'd', 'e']

console.log(arr.splice(), arr);

image.png

直接返回一个空数组,原数组arr不受影响。

当传一个参数时:

let arr = ['a', 'b', 'c', 'd', 'e']

console.log(arr.splice(1), arr);

image.png

我们只传了一个1,它就会从下标1开始切到数组最后一位,存放到一个新数组中返回,并且原数组也受影响。

当我们传两个参数时:

let arr = ['a', 'b', 'c', 'd', 'e']

console.log(arr.splice(1, 2), arr);

image.png

第一个参数代表从哪个下标开始切,第二个参数代表切几个。我们传了一个1一个2,它就会从下标1开始切两个存放到一个新数组返回,并且原数组受影响。如果第二个参数是0,那就代表切0个,就不会切,就会返回一个空数组。

它还可以传3个参数,那就是往数组中添加值了:

let arr = ['a', 'b', 'c', 'd', 'e']

console.log(arr.splice(1, 1, 'x', 'y'), arr);

image.png

从下标1开始切1个,并将'x', 'y'添加到原数组的下标1身上。

如果第一个参数传undefined:

let arr = ['a', 'b', 'c', 'd', 'e']

console.log(arr.splice(undefined, 1), arr);

image.png

那就会默认从下标0开始切。

如果第二个参数传undefined:

let arr = ['a', 'b', 'c', 'd', 'e']

console.log(arr.splice(1, undefined), arr);

image.png

那就是切0个。

好,开始写我们自己的my_splice。

let arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_splice = function (start, len, ...args) {
   
}

我们定义三个形参,start就代表从哪个下标开始切,len就代表切几个,...args用来收集要添加的元素。

首先,当一个参数都没传时,我们不切,直接返回空数组。

let arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_splice = function (start, len, ...args) {
   // 如果没有任何参数传递,直接返回空数组
    if (arguments.length === 0) {
        return [];
    }
}

arguments可以用来获取函数接收的所有参数,存放到arguments数组里。所以我们去判断它的长度,长度为0时代表一个参数都没传,直接返回空数组。

当start传的是undefined时,会默认从0开始切,所以当start为undefined时,我们设为0。当len传的是undefined时,代表切0个,所以我们也设为0。

let arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_splice = function (start, len, ...args) {
   // 如果没有任何参数传递,直接返回空数组
    if (arguments.length === 0) {
        return [];
    }
    
    // 如果 start 是 undefined,设置为 0
    if (start === undefined) {
        start = 0;
    }

    // 如果 len 是 undefined,设置为 0
    if (len === undefined) {
        len = 0;
    }
}

接下来就开始循环遍历数组了。我们来想一想怎么从数组身上切值下来,并且原数组也要受影响。我们可以提前准备两个数组res和newArr。当 i 循环 到start 和 start+len(左闭右开) 中的值时,我们就push到res中作为返回的新数组,当 i 不在这里面说明不是要切的值,我们就push到newArr中作为原数组。

let arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_splice = function (start, len, ...args) {
   // 如果没有任何参数传递,直接返回空数组
    if (arguments.length === 0) {
        return [];
    }
    
    // 如果 start 是 undefined,设置为 0
    if (start === undefined) {
        start = 0;
    }

    // 如果 len 是 undefined,设置为 0
    if (len === undefined) {
        len = 0;
    }
    
    const res = [], newArr = []
    for (let i = 0; i < this.length; i++) {
        if (i < start || i >= start + len) {
            newArr.push(this[i])
        } else {
            res.push(this[i])
        }
    }
}

还有要添加的值是存放在args里的,所以我们还要把args中的值添加到原数组中去。那什么时候添加呢?因为start是我们要切的第一个值,所以我们要从 tart-1 后面开始往原数组中添加值。所以当i === start - 1我们把args中的所有值都push到newArr中。

let arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_splice = function (start, len, ...args) {
   // 如果没有任何参数传递,直接返回空数组
    if (arguments.length === 0) {
        return [];
    }
    
    // 如果 start 是 undefined,设置为 0
    if (start === undefined) {
        start = 0;
    }

    // 如果 len 是 undefined,设置为 0
    if (len === undefined) {
        len = 0;
    }
    
    const res = [], newArr = []
    for (let i = 0; i < this.length; i++) {
        if (i < start || i >= start + len) {
            newArr.push(this[i])
        } else {
            res.push(this[i])
        }
        if (i === start - 1) {
            newArr.push(...args)
        }
    }
}

那接下来就是把newArr赋会给原数组,再返回新数组。

let arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_splice = function (start, len, ...args) {
   // 如果没有任何参数传递,直接返回空数组
    if (arguments.length === 0) {
        return [];
    }
    
    // 如果 start 是 undefined,设置为 0
    if (start === undefined) {
        start = 0;
    }

    // 如果 len 是 undefined,设置为 0
    if (len === undefined) {
        len = 0;
    }
    
    const res = [], newArr = []
    for (let i = 0; i < this.length; i++) {
        if (i < start || i >= start + len) {
            newArr.push(this[i])
        } else {
            res.push(this[i])
        }
        if (i === start - 1) {
            newArr.push(...args)
        }
    }
    
    while (this.length) {
        this.pop()
    }
    this.push(...newArr)
    return res
}

这样就写完了,应该没有什么大问题。各位可以自行去测试一下,如果还有没考虑到的情况,各位可以指正一下。写完这篇文章我已经浑身无力了,如果对你有帮助的话请点个赞吧,感谢!