【JS】JavaScript中的数组方式封装(包括两种sort写法)

        在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));
        };

        以上就是一些数组的实现原理和封装逻辑,熟练掌握有助于加强业务中需要运用的一些简单算法的计算。 

猜你喜欢

转载自blog.csdn.net/a1611107035/article/details/127635129
今日推荐