前端查缺补漏系列(二)JS数组及其扩展

一. 前言

之前介绍了JS的对象以及其扩展内容,这次就轮到数组了。数组在日常的前端开发中经常使用,但是还是有必要讲一下数组的一些用法以及扩展。还是一样的,欢迎各位jym们来查缺补漏

二. 数组方法

这里先列举一些常见的JS数组方法,以及其合理使用的场景。顺便来看一下其原理如何实现。

2.1 Array.prototype.forEach()

是 JavaScript 数组的内置方法,用于遍历数组元素并执行回调函数。

js 复制代码
/**
 * callbackFn: 回调函数,包含3个参数:item(正在处理的当前元素)index(当前索引) Array(数组本身)
 * thisArg: 执行callbackFn的this指向。

 返回值: void
 */
forEach(callbackFn, thisArg)

2.1.1 使用

js 复制代码
const numbers = [1, 2, 3];  
numbers.forEach(item => {
    console.log(item)  // 1, 2, 3 
})

一般来说,forEach的本身不会改变数组,但是你可以通过callbackFn的内部逻辑去改变。

举个例子。

js 复制代码
const numbers = [1, 2, 3];  
numbers.forEach(item => {
    item *= 2
})
console.log(numbers) // [1, 2, 3]

这里并没有改变数组内部元素,因为forEach但并没有提供直接修改数组本身的方式。如果坚持要用forEach来修改,那就通过数组下标的方式来更改数组。

js 复制代码
const numbers = [1, 2, 3];  
numbers.forEach((item, index) => {
    numbers[index] = item * 2
})
console.log(numbers) // [2, 4, 6]

forEach的第二个参数thisArg,是改变callBack内部执行时的this指向。这里要注意,如果显式的传入thisArg,那么外层的callBack回调要注意使用箭头函数。举个例子:

js 复制代码
const arr = [1];  
const context = { value: 'new value' }
// 情况1: callBack是普通函数,不传thisArg的this指向。
arr.forEach(function(item) {
  console.log(this)  // Window {......}
})

// 情况2: callBack是普通函数,传thisArg的this指向。
arr.forEach(function(item) {
  console.log(this)  // {value: 'new value'}
}, context)

// 情况3: callBack是箭头函数,不传thisArg的this指向。
arr.forEach(item => {
  console.log(this)  //  // Window {......}
})

// 情况4: callBack是箭头函数,传thisArg的this指向。
arr.forEach(item => {
  console.log(this)  //  // Window {......}
}, context)

因为箭头函数没有自己的this指向,所以情况4箭头的this指向是它外层的非箭头函数的this指向,所以是window。

有几个注意点:

  • forEach 不能被中断循环1,return或者break没有作用(直接使用break,会报非法中断语句错误)。如果非要跳出循环,可以使用try catch 或者 每次循环减少数据元素(不建议这么做)。
  • forEach 没有返回值,不支持链式调用,如果需要请放到最后一位

2.1.2 实现

简单实现一下forEach,具体步骤如下:

  • 接受两个参数:callBackthisArgs(非必传)
  • 获取外部传入的this: 通过arguments获取。
  • 获取原数组: 通过内部的this获取调用该方法的原数组。
  • 遍历原数组: 对原数组的每一项进行遍历,同时执行callBack,传入三个参数(item, index, array)
js 复制代码
Array.prototype.myForEach = function (callback) {
    var ctx = arguments[1] || window;
    var originArray = this;
    var len = originArray.length;
    for (var i = 0 ; i < len ; i++) {
        callback.apply(ctx, [originArray[i], i, originArray])
    }
}

2.2 Array.prototype.map()

map() 方法创建一个新数组 ,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成。不会改变原数组

js 复制代码
/**
* callbackFn: 回调函数,包含3个参数:item(正在处理的当前元素)index(当前索引) Array(数组本身) 
* thisArg: 执行callbackFn的this指向。 
* 返回值: 每项执行后的结果。
*/
map(callbackFn, thisArg)

2.2.1 使用

js 复制代码
const arr = [
    {name: '111', id: 1},
    {name: '222', id: 2},
    {name: '333', id: 3}
]

const arr2 = arr.map(item => item.id) //  [1, 2, 3]

这里注意的是,map操作不会改变原始数组,而且生成的新的数组是深拷贝

js 复制代码
const arr = [1, 2, 3]
const arr2 = arr.map(item => item = item * 2 )
console.log(arr)   // [1, 2, 3]
console.log(arr2)  // [2, 4, 6]
arr[0] = 10
console.log(arr)   // [10, 2, 3]
console.log(arr2)  // [2, 4, 6]
// 可以看出,单独修改arr的某一项,并不会对arr2有影响

map的第二个参数,用法和forEach相似,这里就不过多描述了。

2.2.2 实现

其实map的实现步骤和forEach类似。但是要注意几点不同。

  • map是有返回值的,返回值是一个数组,需要把callback执行的结果push进去
  • 需要对生成的新数组的每一项深拷贝一下。
js 复制代码
Array.prototype.myMap = function(cb) {
  var ctx = arguments[1] || window;
  var array = this;
  var len = array.length;
  var newArr = [];
  for (var i = 0 ; i < len ; i++) {
    newArr.push(cb.apply(ctx, [array[i], i, array]))  // 这里其实需要深拷贝一下,暂时先这样简单实现
  }
  return newArr
}

2.3 Array.prototype.filter()

通过filter方法,对指定数组项进行过滤,返回一个数组。不会改变原数组

js 复制代码
/**
* callbackFn: 回调函数,包含3个参数:item(正在处理的当前元素)index(当前索引) Array(数组本身) 
* thisArg: 执行callbackFn的this指向。 
* 返回值: 每项执行后的结果。
*/
filter(callbackFn, thisArg)

2.3.1 使用

javascript 复制代码
const arr1 = [1, 2, 3, 4, 5, 6]
const arr2 = arr1.filter(num => num > 3)
console.log(arr2) //  [4, 5, 6]
arr1[3] = 8
console.log(arr1)  // [1, 2, 3, 8, 5, 6]
console.log(arr2)  //  [4, 5, 6]

filter的第二个参数同上。

2.3.2 实现

具体的实现步骤,跟如上流程差不多,有几点要注意一下。

  • 返回一个新的数组。
  • 返回的数组包含的值是满足callback函数中执行的结果,为true则放入新数组中。
js 复制代码
Array.prototype.myFilter = function (cb) {
  var ctx = arguments[1] || window;
  var array = this;
  var len = array.length;
  var newArr = [];
  
  for (var i = 0 ; i < len ; i++) {
    if (cb.apply(ctx, [array[i], i, array])) {  // 判断当前cb执行的结果,是否为true
      newArr.push(array[i])
    }
  }
  return newArr
}

2.4 Array.prototype.every()

every() 方法测试一个数组内的所有元素是否都能通过callback函数。它返回 一个布尔值

这里要保证每一项都通过callback函数,有一项不满足就返回false

js 复制代码
/**
* callbackFn: 回调函数,包含3个参数:item(正在处理的当前元素)index(当前索引) Array(数组本身) 
* thisArg: 执行callbackFn的this指向。 
* 返回值: 每项执行后的结果。
*/
every(callbackFn, thisArg)

2.4.1 使用

js 复制代码
const arr = [1, 2, 3, 4, 5]
const res1 = arr.every(arr => arr > 4)  // false
const res2 = arr.every(arr => arr > 0)   // true

没什么好说的,使用起来也比较简单。第二个参数的使用如上

2.4.2 实现

同样的流程跟之前差不多,注意以下几点:

  • 返回的结果为true or false, 所以要定义一个temp作为返回的结果。
  • 如果有一项不满足callback的结果,直接把temp设为false,然后break终止循环。
js 复制代码
Array.prototype.myEvery = function(cb) {
  var ctx = arguments[1] || window;
  var array = this;
  var len = array.length;
  var temp = true;  // 这里初始化一定要设置为true, 因为判断有一项不满足就要跳出循环,然后返回。

  for (let i = 0 ; i < len ; i++) {
    if (!cb.apply(ctx, [array[i], i, array])) {
      temp = false;
      break;   // 直接跳出循环,后续不需要判断
    }
  }

  return temp
}

2.5 Array.prototype.some()

some() 方法测试数组中是否至少有一个元素通过了由提供的函数实现的测试。如果在数组中找到一个元素使得提供的函数返回 true,则返回 true;否则返回 false。它不会修改数组

这里要保证有一项都通过callback函数即可,返回true。

js 复制代码
/**
* callbackFn: 回调函数,包含3个参数:item(正在处理的当前元素)index(当前索引) Array(数组本身) 
* thisArg: 执行callbackFn的this指向。 
* 返回值: 每项执行后的结果。
*/
some(callbackFn, thisArg)

2.5.1 使用

js 复制代码
const arr = [1, 2, 3, 4, 5]
const res1 = arr.some(arr => arr > 4)  // true
const res2 = arr.some(arr => arr > 6)   // false

没什么好说的,使用起来也比较简单。第二个参数的使用如上

2.5.2 实现

every实现的流程极其相似。要注意以下几点不同

  • 只要callback函数有一个结果满足,就返回true,然后终止循环。
js 复制代码
Array.prototype.myEvery = function(cb) {
  var ctx = arguments[1] || window;
  var array = this;
  var len = array.length;
  var temp = false;  // 这里初始化一定要设置为false, 因为判断有一项满足就要跳出循环,然后返回true。

  for (let i = 0 ; i < len ; i++) {
    if (cb.apply(ctx, [array[i], i, array])) {
      temp = true;
      break;   // 直接跳出循环,后续不需要判断
    }
  }

  return temp
}

2.6 Array.prototype.reduce()

reduce() 方法对数组中的每个元素按序执行一个提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值。

reduce的参数和之前的一些方法有所不同,这里细讲一下。

reduce(callback, initialValue)

  • callback : 回调函数,其返回值将作为下一次调用 callback 时的 accumulator 参数。最后一次执行时,作为结果返回出去。接受以下参数 accumulator,currentValue,currentIndexarray
  • accumulator : 上一次执行回调函数的结果。在第一次调用时,如果指定了 initialValue 则为指定的值,否则为 array[0] 的值。
  • currentValue :当前元素的值。在第一次调用时,如果指定了 initialValue,则为 array[0] 的值,否则为 array[1]
  • currentIndex : 在数组中的索引位置。在第一次调用时,如果指定了 initialValue 则为 0,否则为 1。 -array :调用了 reduce() 的数组本身。

2.6.1 使用

reduce累计运算。

js 复制代码
const arr = [1, 2, 3, 4, 5]
const res = arr.reduce((acc, current) => acc + current, 0)
console.log(res)   // 15

reduce 去重

js 复制代码
function unique(arr) {
  return arr.reduce((acc, current) => {
    return acc.includes(current) ? acc : [...acc, current]
  }, [])
}
unique([1, 1, 2, 2, 4, 5, 6, 6, 7])  // (6) [1, 2, 4, 5, 6, 7]

2.6.2 实现

实现的话,核心实现就是把initialvalue当作每次执行的结果,循环执行完成之后,把它return出去就可以了。

js 复制代码
Array.prototype.myReduce = function (callBack, initialvalue) {
    var _arr = this;
    var _len = _arr.length;
    for (var i = 0; i < _len; i++) {
        initialvalue = callBack(initialvalue, _arr[i], i, _arr);  
        // 第一次的initialvalue为传入的值,然后每次执行的initialvalue都是上次的结果。
    }
    return initialvalue;
}

三. 数组转换

这里主要考虑的是,哪些数据类型可以转换为数组。

3.1 类数组结构

在日常的开发中,类数组结构其实还是比较常见的。什么是类数组结构?

  • 索引访问 ‌:使用数字索引访问元素(如通过键名访问)。
  • 长度属性 ‌:具备length属性表示元素数量。
  • 方法缺失 ‌:不支持数组原生方法(如pushsplice等),需通过其他方式操作数据。

举例子:

类数组结构的对象。

js 复制代码
const obj = {0: 'a', 1: 'b', length: 2}   // 每一项的下标从0开始,总数和length对应

NodeList对象

js 复制代码
let ps = document.querySelectorAll('p');

函数的arguments

js 复制代码
function fn() {
    console.log(arguments)
}

3.2 部署Iterator接口

这个后续会细讲(挖个坑,自己肯定会更新😭),这里只要知道哪些数据结构部署了iterator接口??

  • string
  • Map
  • Set
  • 数组本身

  • 上面提到的 NodeListarguments

3.3 转换方法

3.3.1 \[\].slice.call(arrayLike)

js 复制代码
const obj = {0: 'a', 1: 'b', length: 2} 
const arr = [].slice.call(obj)  // (2) ['a', 'b']

为什么这样写可以实现转为数组?

首先,slice方法可以接受一个空值,结果返回调用的原数组

js 复制代码
let a = [1, 2, 3]
a.slice()

slice是一个函数,我们可以通过call方法,改变this指向,让slice指向类数组

\[\].slice.call(arrayLike)

然后slice内部会根据this指向的对象的length属性的长度,来确定截取的范围。而类数组结构含有length属性,所以就会转为数组。

3.3.2 Array.prototype.slice.call(arrayLike)

使用和上面类型,原理也相同,这里就不过多bb了。

这里要注意一下: 上面两种方法,本质上都是依靠类数组对象有length属性完成的转换,和iterator接口无关。

3.3.3 Array.from()

有length属性的对象和部署了和iterator接口的对象都可以转为数组。

js 复制代码
// 有length属性的对象
const obj = {0: 'a', 1: 'b', length: 2} 
Array.from(obj)  // (2) ['a', 'b']
js 复制代码
// 部署了和iterator接口
const set = new Set([1, 2, 3])
Array.from(set)  // (3) [1, 2, 3]
python 复制代码
// 转换字符串
Array.from('hello')  // (5) ['h', 'e', 'l', 'l', 'o']

3.3.4 扩展运算符

在ES6里面的扩展运算符,也可以实现类数组转数组,这个原理也是部署了iterator接口

js 复制代码
const set = new Set(['s', 't'])
const a1 = [...set]

四. 其他

4.1 Array.of()

Array.of方法用于将一组值,转换为数组。

js 复制代码
Array.of(1, 2, 3) // (3) [1, 2, 3]

这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异

js 复制代码
Array()  // []
Array(3)  // (3) [empty × 3]
Array(1, 3) // (2) [1, 3]

Array.of总是返回参数值组成的数组。如果没有参数,就返回一个空数组

五. 总结

这里简单的介绍了一些数组的基本方法和使用以及简单实现流程。时间有限,其他的方法暂时就先不介绍了,大家有兴趣可以多评论交流哈!!

相关推荐
研☆香8 小时前
闭包实战避坑指南
javascript
没落英雄9 小时前
6. 从零搭建一个 AI Agent —— 构建 Web 前端,让用户能和 agent 实时交互
前端·人工智能·架构
爱勇宝9 小时前
《道德经》第4章,老子写得很像在描述一个“底层系统”。
前端·后端·程序员
你怎么知道我是队长9 小时前
JavaScript 的控制语句介绍
开发语言·javascript·ecmascript
艾莉丝努力练剑9 小时前
【AI面试】AI八股文
人工智能·ai·面试·langchain·八股文
帅次9 小时前
Android 高级工程师面试:Flutter Widget 体系 近1年高频追问 20 题
android·flutter·面试·element·widget·setstate·renderobject
snow@li9 小时前
Java:Java 服务器(Web 容器 / Servlet 容器)完整工作清单
java·服务器·前端
帅次10 小时前
Android 高级工程师面试:Jetpack 核心 近1年高频追问 20 题
android·面试·协程·jetpack·stateflow·lifecycle
用户440145493141910 小时前
Spring Boot 3.x 异步编程实战:从 @Async 到虚拟线程
javascript