学习关键语句:
Array.reduce
Array.prototype.reduce
reduce方法
重写 reduce 方法
1. 写在前面
很多同学 ( 指我自己 ) 在学习其他数组扩展方法时都没那么困难 , 但是到了 reduce 方法时就会显得蠢蠢的 , 所以今天就赶紧将这个方法讲个明白
其实所有的数组扩展方法本质上都是在用循环和遍历 , 所以其实非常简单 , 今天不仅要讲清楚 reduce 的用法 , 还要用 reduce 来模拟其他数组扩展方法
那么赶紧来一起看一看吧
2. 开始
2.1 reduce 的使用方法
reduce 是数组原型对象上的方法 , 所以可以直接从一个数组调用
我们先看 reduce 的使用形式, 如下代码
let initValue = null;
let newArr = arr.reduce(function (pre, cur, index, arr) {
// 业务代码
}, initValue)
我们看到一共有 4 个形参和 1 个实参 , 分别是
形参 : pre / cur / index / arr
实参 : initValue
我们讲一讲这些都是什么 ( 讲人话 )
- pre : 第一次接收传入的实参 initValue 值 / 除了第一次以外每次接收上次函数返回的值
- cur : 当前循环到的数组元素
- index : 当前循环到的数组的索引
- arr : 当前循环的数组
- initValue : 第一次手动给 pre 的值
reduce 方法中会不停的调用第一个参数函数 , 所以 pre 除了第一次的值是手动传入以外 , 接下来每一次的值都是上一次参数函数的返回值
怎么确定 pre 第一次是用的 initValue 的值 ?
let arr = [1]; let initValue = Math.random(); let newArr = arr.reduce(function (pre, cur, index, arr) { console.log(pre === initValue); // true }, initValue)
所以 reduce 并不是用来解决什么具体问题的方法 , 它是一个自由度很高的方法
我们来看两个案例
2.1.1 案例一 : 数组的使用
需求 : 获取到 data 中的人名
// 用 reduce 方法获取到每个对象中的人名
let data = [
{ id: 1, name: '张三', eat: true, age: 24 },
{ id: 2, name: '李四', eat: false, age: 15 },
{ id: 3, name: '王五', eat: false, age: 44 },
{ id: 4, name: '赵六', eat: true, age: 22 },
{ id: 5, name: '林七', eat: true, age: 24 },
]
let initValue = [];
let nameList = data.reduce(function (pre, cur, index, arr) {
// 第一次的 pre 的值是 initValue 传进来的是一个空的对象
// 每次将 cur 当前循环到的数组元素的 name 值放进 pre 中
// 并且将 pre 返回作为这个函数下一次运行时的 pre 的初始值
pre.push(cur.name);
return pre;
}, initValue)
console.log(nameList);
2.1.2 案例二 : 对象的使用
需求 : 将浏览器 cookie 值取出并改为对象形式
// 获取浏览器 cookie
// 这里的 cookie 你可以随便打开一个网页从控制台输入 document.cookie 获取
let cookie = "BIDUPSID=DC121AD5196A1; PSTM=16859; BAIDUID=DC121AD5196A1B69390521:FG=1; BD_UPN=123143; MCITY=-19%3A; sugstore=0; H_PS_PSSID=3964; BAIDUID_BFESS=DC121AD519918590521:FG=1; b-user-id=11b5db-7f7c-ca96-7b82-f9338e5; ZFY=gsXyFw:AG:BaAtZlSnw4Z5YZop6iGeZmA:C; RT=\"z=1&dm=baidu.com&si=158ae890-f910-4a2e-b2ff-7c56&ss=lobi6w&sl=2&tt=1et&bcn=https%3A%2F%2Ffg.baidu.com%2Flog%2Fweirwood%3Ftype%3Dperf&ld=ok&ul=4ztx&d=4zvu\"; BA_HCTOR=0k2k2g8a5212408501l251l1611q; BDORZ=4905EFF3D40215D2BDA1598; Hm_lvt_aec69bb642b076c891cdc49771=16964582,16996550,16921029,19997199; COOKIE_SESSION=4682_2_6_9_3_6_1_0_6_9_1_3_38695_24088_0_5_16995956_16994134_16994129%7C9%23247092_108_16844129%7C9"
// 将一长串字符串分割为以等号相连的数组
let cookieArr = cookie.split(';')
// 对每个数组进行循环操作
let cookieObj = cookieArr.reduce((pre, item) => {
// 将每项分割为两个元素的数组, 分别是键名和键值
let arr = item.split('=');
// 第一次为传入的空对象, 每次给对象写入新属性
pre[arr[0]] = arr[1];
// 返回 pre 给下一次的参数函数
return pre;
}, {})
console.log(cookieObj);
其实案例说明不了什么 , 就像我上面说的 , reduce 方法太自由了 , 基本上什么都可以实现 , 但是还有一个问题 , 那就是 reduce 方法是 ES5 提出来的 , 没办法让它兼容低版本 , 我们只能重写自己的 reduce 方法
2.2 重写 reduce 方法
想要重写某个数组方法 , 必须先看这个方法的使用方法 , 我们上面已经了解过了
所以我们知道 , 我们的 reduce 方法需要接收两个参数 , 一个是回调函数 , 一个是初始值
Array.prototype.myReduce = function (fn, initValue) {
var arr = this;
for (var i = 0; i < arr.length; i++) {
// 第一次执行传入的 fn 方法时, pre 的值就由 initValue 直接传入
// 每次执行完 fn 方法后, 将返回值赋给 initValue 并进行下一次循环
// 每一次新的循环执行 fn 方法都会用到上一次循环返回的 initValue 值
// 即每次 pre 的值都是上一次循环返回的值
initValue = fn(initValue, arr[i], i, arr);
}
return initValue;
}
但是我们知道其他的数组扩展方法是可以指定函数中的 this 指向的 , 所以我们稍微修改一下
Array.prototype.myReduce = function (fn, initValue) {
var arr = this,
// 如果用户没有传入指定的 this 指向那就指向 window
arg2 = arguments[2] || window;
for (var i = 0; i < arr.length; i++) {
// 使用 call 改变 fn 函数的 this 指向, 其他不变
initValue = fn.call(arg2, initValue, arr[i], i, arr);
}
return initValue;
}
2.3 reduce 模拟其他数组方法
reduce 方法是一个自由度很高的方法 , 甚至可以用它完成其他的数组扩展方法 , 我们现在就来试一试
2.3.1 reduce 模拟 forEach 方法
forEach 是最常见的扩展方法之一了 , 用 reduce 模拟显得太鸡肋 , reduce 在其中的作用是循环数组
Array.prototype.myForEach = function (fn) {
var arr = this,
arg2 = arguments[1] || window;
arr.reduce(function (pre, cur, index, arr) {
// forEach 中需要的参数分别是元素, 索引, 数组, 一一对上写就好了
fn.call(arg2, cur, index, arr);
// 循环遍历不需要 return
}, null)
}
2.3.2 reduce 模拟 filter 方法
reduce 在 filter 中的作用是循环数组
Array.prototype.myFilter = function (fn) {
var arr = this,
arg2 = arguments[1] || window,
res = [];
arr.reduce(function (pre, cur, index, arr) {
// 参数函数返回值为真的情况下需要将这一元素放入返回数组中
fn.call(arg2, cur, index, arr) ? res.push(cur) : ''
}, null)
return res;
}
2.3.3 reduce 模拟 map 方法
reduce 在 map 中的作用是循环数组
Array.prototype.myMap = function (fn) {
var arr = this,
arg2 = arguments[1] || window,
res = [];
arr.reduce(function (pre, cur, index, arr) {
// 直接将参数函数返回值放入返回数组中
res.push(fn.call(arg2, cur, index, arr))
}, null)
return res;
}
2.3.4 reduce 模拟 every 和 some 方法
reduce 在 every 中的作用是循环数组
Array.prototype.myEvery = function (fn) {
var arr = this,
arg2 = arguments[1] || window,
res = true;
arr.reduce(function (pre, cur, index, arr) {
// 只要有一次参数函数返回了 false 就返回 false
if (!fn.call(arg2, cur, index, arr)) {
res = false;
}
}, null)
return res;
}
reduce 在 some 中的作用是循环数组
Array.prototype.mySome = function (fn) {
var arr = this,
arg2 = arguments[1] || window,
res = false;
arr.reduce(function (pre, cur, index, arr) {
// 只要有一次参数函数返回了 true 就返回 true
if (fn.call(arg2, cur, index, arr)) {
res = true;
}
}, null)
return res;
}
3. 总结
reduce 作为 ES5 推出的数组扩展方法 , 也是前端必须要学习的东西之一 , 同时我们在重写以及模拟其他方法的过程中 , 我们更能清晰的明白扩展方法其实本质上就是循环遍历然后调用传入的函数而已
4. 结束
reduce 的讲解就告一段落 , 希望你真的有所收获 , 并且在模拟这一部分用 for 循环代替 reduce 方法的话就是其他数组方法的重写了
如果你跟着做遇到了什么问题无法实现 , 请在评论区或者私信告诉我 , 我发动网友给你解答