在JavaScript的ES6中新增了一些十分有用的数组遍历方式。尽管他们特别重要,但他们本质上都是语法糖。也就是说,我们可以比较轻松地封装我们自己的数组方式。本文将从forEach, map,filter,和every开始,到最后使用两种种方式封装数组排序的方法
为了能充分地理解本文的内容,你需要对这些数组方式,包括JavaScript中的this有一定地了解。如果你不是很了解这两个话题,我建议你在了解this和数组遍历方式后,再来阅读此文章。
准备工作:
在我们正式开始前,我们先复习一下this的意义。this是一个对象,在一般情况下代指函数的调用者。我们在使用数组方式的时候写法通常是array.methods()。也就是说,方法中的this其实指的就是数组本身。我们会大量使用this来指代被遍历的数组本身。
因此,需要格外注意的是,你在定义这些数组函数的时候不能使用箭头函数。因为箭头函数不自带this。因此,this会变成undefined,使我们的逻辑失效。
所有的数组遍历都将使用for循环来实现。
1. forEach()
我们仍然从一个比较简单的开始。如果你不记得的话,forEach接受一个回调函数,回调函数有三个形参,分别是element,index,和array。forEach没有返回值。在了解这些条件后,我们可以直接在Array的原型上自定义方式:
// forEach 接受一个回调函数作为参数,回调函数的参数分别是element,index和array
Array.prototype.myForEach = function (callback) {
for (var i = 0; i < this.length; i++)
// 回调中的形参分别是element,index,和array本身
callback(this[i], i, this);
};
// 测试
var arr = ['biggy smalls', 'bif tannin', 'boo radley', 'hans gruber'];
arr.myForEach(function (word) {
console.log(word);
});
2. map()
map和forEach的逻辑很像,最大的区别在于map是有返回值的,并且返回的是
一个新数组。因此,我们要现在map内定义一个临时的数组变量,然后在最终遍历完成后,
把这个数组返回出来
Array.prototype.myMap = function (callback) {
arr = [];
for (let i = 0; i < this.length; i++)
// 把每轮回调函数对元素的改变后的值插入新的数组当中
arr.push(callback(this[i], i, this));
return arr;
};
//tests
var numbers2 = [1, 4, 9];
var squareRoot = numbers2.myMap(function (num) {
return Math.sqrt(num);
});
3. every()
every虽说用法上最简单,但实现逻辑上需要稍微有一些思考。我们需要每轮回调的结果都为true,直到出现false或者遍历结束为止。也就是说,当我们发现第一个false的时候,就需要终止遍历,并且返回false。
Array.prototype.myEvery = function(callback) {
for(let i = 0; i < this.length; i++) {
// 遍历检查是否每个回调的结果都为true,若发现一个false,立刻返回false并终止循环
if(!callback(this[i], i, this)) {
return false
}
}
return true
}
passed = [12, 54, 18, 130, 44].myEvery(function(element) {
return (element >= 13);
});
console.log(passed); // false
在正常情况下,我们刚才的every封装已经足够了,但在真正的every中其实还有第二个隐藏参数,是一个this。该this用于更改every在执行时this的指向。通常我们用不得这个参数,但为了完整还原every的功能,我们把这个功能也添加进去
Array.prototype.myEvery = function(callback, context) {
for(let i = 0; i < this.length; i++) {
// 利用call()来更改this的指向
if(!callback.call(context, this[i], i, this)) {
return false
}
}
return true
}
4. filter()
filter会把每一个满足条件的元素添加到一个新数组中,返回的这个新数组是原数组的拷贝。
我们需要再此用到push,但这个我们只需要push元素本身的值即可。并且,和every有类似的问题,filter自身也有一个可选并且极其不常用的第二参数:this。为了封装的完整性我们把它写进去。
Array.prototype.myFilter = function(callback) {
arr = []
for (let i = 0; i < this.length; i++) {
// 如果回调函数的返回值为true,则把该元素添加到新元素当中
if (callback(this[i], i, this)) {
arr.push(this[i])
}
}
console.log(arr)
return arr
}
var numbers = [1, 20, 30, 80, 2, 9, 3];
var newNum = numbers.myFilter(function(n) {
return n >= 10;
});
console.log(newNum); // [ 20, 30, 80 ]
// 完整版
Array.prototype.myFilter2 = function(callback, context) {
arr = []
for(let i = 0; i < this.length; i++) {
// 利用call()来更改this的指向
if(callback.call(context, this[i], i, this)) {
arr.push(this[i])
}
}
return arr
}
5. reduce()
reduce的封装难度比起之前的几个就要明显大一些,我们其中有几个更复杂的逻辑判断。首先,我们复习一下reduce的语法:
reduce(function(previousValue, currentValue, currentIndex, array) { /* … */ }
, initialValue)
reduce的回调函数总共有四个参数,除第一个外后三个和前面相同。第一个参数是之前所有遍历的总值,有人叫他previousValue,但其实总值更符合对他的形容。我们待会声明一个accumulator来代表总值。另外,reduce还有一个可选的第二参数: 初始值。这个值如果存在的话,那么他将成为accumulator的初始值。如果不存在的话,accumulator的初始值就是undefined。由此,我们可以写出代码的第一步:
Array.prototype.myReduce = function(callback, initialValue) {
let accumulator
if (initialValue) {
accumulator = initialValue
} else {
accumulator = undefined
}
}
// 我们还可以通过进一步简写,用三元表达式来代替if, else
Array.prototype.myReduce = function(callback, initialValue) {
let accumulator
// if (initialValue) {
// accumulator = initialValue
// } else {
// accumulator = undefined
// }
initialValue ? accumulator = initialValue : accumulator = undefined
}
接下来,我们就可以开始进行遍历了。同样使用for循环的方式,但是在循环当中,我们需要再此判断accumulator是不是undefined,因为初始值并不一定存在。如果accumulator确实是undefined,我们就把遍历中的元素赋值给它。如果本身已经有值,我们就调用回调函数,然后分别传入accumulator,element,index和array四个参数。最后,我们把accumulator的值返回出来。
Array.prototype.myReduce = function(callback, initialValue) {
let accumulator
// if (initialValue) {
// accumulator = initialValue
// } else {
// accumulator = undefined
// }
initialValue ? accumulator = initialValue : accumulator = undefined
for(let i = 0; i<this.length; i++) {
if(accumulator !== undefined) {
accumulator = callback(accumulator, this[i], i, this)
} else {
accumulator = this[i]
}
}
return accumulator
}
6-1. sort() 冒泡式
别看sort自身并不难理解,但是封装sort确是难度最大的一个。自己写sort的实现是一个经典的面试题和算法入门题目。我们这里讲解三种不同的sort实现思路。首先,从冒泡开始:
冒泡的基本思路是在一个循环内部再进行一个循环。如下:
for(let i = 0; i < arr.length; i++){
for(let j = 0; j < arr.length - i - 1; j++){
}
}
};
外部循环的意义很好理解,就是遍历这个数组中的每个元素。而内部循环是为了比较每两个元素的大小,如果发现后一个比前一个大,则交换他们的位置,否则位置不变。内部循环的次数取决于外部循环的进度:
1- 在第一轮内部循环的时候,内部循环会把数组第一个值和第二个值作比较
2- 如果第一个值更大,则位置不变,并且一个值继续往后比较3
3- 若第二个值大,第一个值就和第二个值交换位置,然后用更大的那个值(也就是第二个值)和后面继续比较
4- 一轮内部循环结束后,从下一个元素开始第下一轮内部循环,直到外部循环结束为止
function bubbleSort(arr) {
for(let i = 0; i < arr.length; i++) {
for(let j = 0; j < arr.length - i - 1; j++) {
if(arr[j] < arr[j+1]) {
[arr[j], arr[j+1]] = [arr[j+1], arr[j]]
}
}
}
return arr
}
6-2. sort() 快排式
快排的主要思路是先找出一个中间数,然后以这个中间数为基准将数组分为两组;我们挨个将小于中间数的元素放在数组1,再把大于中间数的元素放在数组2。然后,我们再对这两个新数组重新调用快排函数。也就是说,我们在分出两个数组后,再分别对这两个数组进行快排,最终变成四个数组。如此反复,直到我们把每个元素都过滤一遍,最后再把他们拼接起来。
1. 定义中间数,定义两个空数组,分别代表小于和大于中间数的新数组
2. 定义for循环,在循环内部判断元素是否大于中间值,根据结果将元素放到数组1或者数组2中
3. 继续快排数组1和数组2,并且将他们的结果拼接起来
4.在快排开始前进行判断,若该数组只有一个值,则直接返回,避免无限调用的发生
function quicksort(array) {
// 快排结束的条件
if (array.length <= 1) {
return array;
}
// 定义中间值
var pivot = array[0];
// 定义数组1和数组2
var left = [];
var right = [];
// 循环判断元素和中间值的大小
for (var i = 1; i < array.length; i++) {
array[i] < pivot ? left.push(array[i]) : right.push(array[i]);
}
// 继续分解,直到所有元素都完成遍历
return quicksort(left).concat(pivot, quicksort(right));
};
以上就是一些数组的实现原理和封装逻辑,熟练掌握有助于加强业务中需要运用的一些简单算法的计算。