当前位置:主页 > 查看内容

JS 原生方法原理探究(六):手写实现 30 个数组原生 API

发布时间:2021-06-08 00:00| 位朋友查看

简介:这是JS 原生方法原理探究系列的第六篇文章,这次我们来实现数组的 30 个 API。在开始之前,可以先看一下本文的思维导图: 文章分为四个部分,分别介绍在数组原生 API 中,会修改原数组的方法、不会修改原数组的方法、用于遍历的方法以及静态方法,共计 30 个……

这是JS 原生方法原理探究系列的第六篇文章,这次我们来实现数组的 30 个 API。在开始之前,可以先看一下本文的思维导图:

文章分为四个部分,分别介绍在数组原生 API 中,会修改原数组的方法、不会修改原数组的方法、用于遍历的方法以及静态方法,共计 30 个。在讲每个方法的具体实现之前,会简要介绍它们的用法(更详细的查阅 MDN 即可),之后给出实现的思路和具体的代码。代码来源于自己思考以及对 polyfill 的参考,实测可以通过大部分测试用例,但不排除有更好的思路以及值得优化的地方,若发现任何错误或者值得改进之处,欢迎评论区留言指正。

这是一些实现时需要注意的地方:

  • 原型方法挂载在数组原型上。由于方法是通过数组实例调用的,所以我们可以在方法内部通过 this 拿到调用者,也就是数组(如果怕修改到原数组,可以把这个 this 浅拷贝一份)
  • 大部分方法在遍历数组的时候,会跳过 empty 元素(空位),而有的方法却不会。因此,在遍历数组的过程中,要注意判断元素是不是 empty 元素 —— 可以用 in 判断,比如索引 1 的元素不是 empty,那么 1 in arr 是会返回 true 的。此外,也可以选择用 for…in 遍历数组,它只会遍历出那些非 empty 元素的索引(注意:for...of 可以遍历出 empty 元素,遍历出的结果是 undefined

下面正文开始。

会改变数组的方法

pop

用法

pop 方法可以弹出数组最后一个元素,并将其作为返回值

const arr = [1,2,3]
arr.pop()                // 返回移除的元素 5,数组变成 [1,2,3,4] 

实现

Array.prototype.myPop = function(){
    let arr = this
    let returnValue = arr[arr.length - 1]
    arr.length--
    return returnValue
}

push

用法

push 方法可以往数组末尾添加任意多个元素,并将数组长度作为返回值:

const arr = [1,2,3]
arr.push(4,5)            // 返回最终的数组长度 5,数组变成 [1,2,3,4,5]

实现

Array.prototype.myPush = function(...args){
    let arr = this
    for(let el of args){
       arr[arr.length] = el
    }
    return arr.length
}

shift

用法

shift 方法可以从数组头部弹出一个元素,并将其作为返回值:

const arr = [3,4,5]
arr.shift()              // 返回移除的元素 1,数组变成 [2,3,4,5]

实现

Array.prototype.myShift = function(){
    let arr = this
    let returnValue = arr[0]
    for(let i = 1;i < arr.length;i++){
        arr[i-1] = arr[i]
    }
    arr.length--
    return returnValue
}

unshift

用法

unshift 方法可以往数组头部添加任意多个元素,并将数组长度作为返回值:

const arr = [3,4,5]
arr.unshift(1,2)         // 返回最终的数组长度 5,数组变成 [1,2,3,4,5]

实现

Array.prototype.myUnshift = function(...args){
    let arr = this
    if(args.length > 0){
        let len1 = arr.length,len2 = args.length
        // k 代表数组最后一个元素的下标
        let k = len1 + len2 - 1
        for(let i = len1 - 1;i >= 0;i--){
            arr[k--] = arr[i]
        }
        for(let i in args){
            arr[i] = args[i]
        }   
    }    
    return arr.length
}

reverse

用法

reverse 将原数组进行反转,最终返回原数组

[1,2,3].reverse()           // [3,2,1]

实现

Array.prototype.myReverse = function(){
    let arr = this
    let k = arr.length - 1
    for(let i = 0;i < Math.floor(arr.length/2);i++){
        let temp = arr[i]
        arr[i] = arr[k]
        arr[k--] = temp
    }
    return arr
}

sort

用法

reverse 也算是一种排序方法,但显然它不够灵活,于是有了 sort 方法。

  • sort 不接受参数或者接受的参数为 undefined 时,默认的排序规则是:将每个元素转化为字符串,再将它们按照 Unicode 编码从小到大排序。其中,null 会转化为 "null"undefined 固定排在数组最后
  • sort 接受参数且为排序函数的时候,按照排序函数的规则排序:若函数返回值为负数,则第一个参数排在第二个参数前面,若为正数,则在它后面,若为 0 则位置不变
const arr = [1,2,5,10]   
// 没有传入函数,会对每个元素调用 toString,比较字符的 unicode 编码,因此 "10"<"2"
arr.sort()                  // [1,10,2,5] 
// 传入比较函数,从小到大排序
arr.sort((a,b) => a<b?-1:a>b?1:0)     // [1,2,5,10]
arr.sort((a,b) => a-b)                // [1,2,5,10]
// 传入比较函数,从大到小排序
arr.sort((a,b) => a>b?-1:a<b?1:0)     // [10,5,2,1]

实现

Array.prototype.mySort = function(...args){
    let arr = this   
    // 判断规则,判断 x 是否应该放在 y 的前面
    function shouldBefore(x,y){
        // 如果没传参或者传了 undefined
        if(args.length == 0 || args.length != 0 && typeof args[0] === 'undefined'){
            return String(x) < String(y)    
        } 
        // 如果传函数
        else {
            let fn = args[0]
            return fn(x,y) < 0 ? true : false
        }
    }
    // 如果传参但是没传函数或者 undefined
    if(typeof args[0] != 'function' && typeof args[0] != 'undefined'){
        throw new TypeError("The argument msut be undefined or a function")
    } else {
        for(let i = 0;i < arr.length - 1;i++){
            for(let j = 0;j < arr.length - 1 - i;j++){
                if(shouldBefore(arr[j+1],arr[j])){
                    // 两数交换
                    let temp = arr[j]
                    arr[j] = arr[j+1]
                    arr[j+1] = temp                  
                }
            }
        }
    }
    return arr
}

splice

用法

splice 可以做三种事:删除元素、添加元素、替换元素。

  • 接受三个参数:开始操作的位置 start、删除的元素个数num ,以及添加的元素 item1、item2、...
  • start 可以是正数或者负数。如果是正数且超过数组长度,则无视删除操作,直接把需要添加的元素添加到数组末尾;如果是负数,且负数绝对值小于数组长度,则将负数与长度相加作为 start,否则将 0 作为 start
  • num 可以是正数或者负数。如果没有传 num,或者 num 是正数且超过 start 往后的元素个数(包含 start),则将 start 和它后面所有元素删除;如果 num 是 0 或者负数,则不删除任何元素
  • 这个方法会修改到原数组,且最终返回一个包含被删除元素的数组,或者空数组
const arr = [1,2,3,4,5]

// 删除:在索引1这里操作,删除2个元素
arr.splice(1,2)                 // 返回 [2,3],arr 变成 [1,4,5]

// 添加:在索引1这里操作,删除0个元素,添加2个元素(注意是插入到索引1前面,不是后面)
arr.splice(1,0,"a","b")          // 返回 [],arr 变成 [1,"a","b",2,3,4,5]

// 替换:删除+添加就是替换
arr.splice(1,2,"a","b")          // 返回 [1,"a","b",4,5]

实现

Array.prototype.mySplice = function(...args){
    let arr = this
    let len = arr.length
    let res = []
    function computeStart(start){
        return start >= 0 ? start : Math.abs(start) < len ? start + len : 0    
    }
    function computeDeleteNum(args,start){
        return args.length < 2 ? 
            len - start : args[1] > 0 ? Math.min(args[1],len - start) : 0   
    }
    function sliceArray(arr,separator){
        let arr1 = [],arr2 = []
        for(let i = 0;i < arr.length;i++){
            i < separator ? arr1.push(arr[i]) : arr2.push(arr[i])
        }
        // 清空原数组
        arr.length = 0
        return [arr1,arr2]
    }
    // 如果有传参数
    if(args.length > 0){
        // 确定 start 和 deleteNum 的取值
        let start = computeStart(args[0])
        let deleteNum = computeDeleteNum(args,start)        
        // 如果 start 已经大于等于数组长度,则只需关注是否有添加元素,无需进行后续操作        
        if(start >= len){
            if(args.length > 2){
                for(let i = 2;i < args.length;i++){
                    arr.push(args[i])
                }   
            }
        } else {
            // 以 start 为界分割原数组
            let [arr1,arr2] = sliceArray(arr,start)
            // 如果有需要,就删除元素
            if(deleteNum != 0){
                for(let i = 0;i < deleteNum;i++){
                    // 把删除的元素放进返回的 res 数组中
                    res.push(arr2.shift())                
                }
            }
            // 如果有需要,就添加元素
            if(args.length > 2){
                for(let i = 2;i < args.length;i++){
                    arr1.push(args[i])
                }
            }    
            const tempArr = [...arr1,...arr2]
            for(let el of tempArr){
                arr.push(el)
            }
        }
    }
    return res
}

PS:个人感觉 splice 的实现算是这几个里比较麻烦的,因为需要考虑很多情况。上面的代码已经通过 MDN 的全部测试用例,但还有不少需要优化的地方。

fill

用法

用某个值替换(填充)数组中的全部值或者部分值:

  • 接受三个参数:toFillbeginendtoFill 表示填充元素,不传则为 undefined;begin 表示开始填充位置,默认从数组第一个元素开始;end 表示结束填充位置(该位置不填充),默认等于数组长度
  • begin 可以是正数或者负数。如果是负数且绝对值小于数组长度,则将其与长度相加作为 begin,若大于数组长度,则取 0 作为 begin
  • end 可以是正数或者负数,如果是正数且超过数组长度,则取数组长度作为 begin;如果是负数且绝对值小于数组长度,则将其与长度相加作为 end
const arr = [0,0,0,0,0]
arr.fill(5)                 // [5,5,5,5,5]
arr.fill(5,2)               // [0,0,5,5,5]
arr.fill(5,2,4)             // [0,0,5,5,0] 
arr.fill(5,-3,-1)           // [0,0,5,5,0]  负值索引 => 负值索引 + 数组长度
arr.fill(5,-100,-90)        // 越界,无效
arr.fill(5,100,90)          // 越界,无效
arr.fill(5,4,2)             // 反向,无效

实现

Array.prototype.myFill = function(toFill,begin = 0,end = this.length){
    let arr = this
    let len = arr.length
    begin = begin >= 0 ? begin : Math.abs(begin) < len ? begin + len : 0
    end = end >= 0 ? Math.min(end,len) : Math.abs(end) < len ? end + len : end
    for(;begin < end;begin++){
        arr[begin] = toFill
    }
    return arr
}

copyWithin

用法

复制数组的某个部分,顶替数组中的某些元素:

  • 接受三个参数,target 表示开始操作的位置,beginend 共同决定需要复制的范围(不包括 end
  • 用范围内的所有元素去覆盖从 target 开始的元素
const arr = [0,1,2,3,4,5,6,7,8]
arr.copyWithin(4)                   // [0,1,2,3,0,1,2,3,4]   缺省范围为整个数组
arr.copyWithin(4,7)                 // [0,1,2,3,7,8,6,7,8]
arr.copyWithin(4,6,9)               // [0,1,2,3,6,7,8,7,8] 

// 对于负值索引、反向索引和越界索引的处理,和 fill 方法类似

实现

Array.prototype.myCopyWithin = function(target = 0,begin = 0,end = this.length){
    let arr = this
    let len = arr.length
    let copyArr = []
    let m = 0,n = 0
    target = target >= 0 ? target : Math.abs(target) < len ? target + len : 0
    begin = begin >= 0 ? begin : Math.abs(begin) < len ? begin + len : 0
    end = end >= 0 ? Math.min(end,len) : Math.abs(end) < len ? end + len : end
    // 把需要复制的元素放到 copyArr 数组中
    for(;begin < end;begin++){
        copyArr[m++] = arr[begin]
    }
    let _len = copyArr.length < len - target ? target + copyArr.length : len
    // 用 copyArr 数组从 target 开始覆盖原数组
    for(;target < _len;target++){
        arr[target] = copyArr[n++]
    }
    return arr
}

不会改变数组的方法

valueOf

用法

对于基本类型的包装对象来说,调用该方法会返回对应的基本类型值,但对于数组一般会直接返回数组本身

const arr = [1,2,3]
arr.valueOf() === arr

实现

Array.prototype.myValueOf = function(){
    return this
}

join

用法

将数组中的每个元素转为字符串并用规定好的分隔符进行连接:

  • 分别对数组每个元素调用一次 toString,之后将这些结果用传给 join 的参数连接起来,返回一个字符串。
  • 如果有 empty 元素,则会被当作 undefined,而 undefinednull 会进一步被转化为空字符串。
[1,2,3].join()                // "1,2,3"  缺省是逗号作为连接符
[1,2,3].join('.')             // "1.2.3"
[{},{},{}].join('**')         // "[object Object]**[object Object]**[object Object]"   

实现

Array.prototype.myJoin = function(connector = ','){
    let arr = this
    let str = ''
    for(x of arr){
        x = typeof(x) === 'undefined' || x === null ? "" : x
        str += x.toString() + connector
    }
    return str.slice(0,str.length - connector.length)
}
// 或者
Array.prototype.myJoin = function(connector = ','){
    let arr = this
    let len = arr.length
    let str = ''
    for(let i = 0;i < len;i++){
        arr[i] = typeof(arr[i]) === 'undefined' || arr[i] === null ? "" : arr[i]
        // 如果是最后一个元素,则不加连接符(后缀符)
        str += arr[i].toString + (i === len - 1 ? '' : connector)
    }
    return str
}

toString

用法

toString 可以看作是 join 的一种特殊情况,即传入的分隔符是逗号,其它的都一样(包括对 undefinednull 和 empty 元素的处理)

[1,2,3].toString()              // "1,2,3"
[{a:1},{b:2}].toString()        // "[obejct Object],[object Object]" 

实现

Array.prototype.myToString = function(){
    let arr = this
    let str = ""
    for(x of arr){
        x = typeof(x) === 'undefined' || x === null ? "" : x
        str += `${x.toString()},`
    }
    return str.slice(0,str.length - 1)
}

concat

用法

concat 可以用于合并数组

  • 可以接受任意多个参数,参数可以是数组或者非数组;
  • 对于非数组,直接将其放入新数组。除非这个非数组是一个类数组对象,且设置了 [Symbol.isConcatSpreadable]=true,此时会取出这个对象的每一项(除了 length)放入新数组
  • 对于数组,取出它的每个元素放入新数组。除非这个数组设置了 [Symbol.isConcatSpreadable]=false

实现

Array.prototype.myConcat = function(...args){
    let arr = this
    let res = []
    let k = 0
    const isArrayLike = obj => {
        if( typeof o === 'object' &&             
               isFinite(o.length) &&                    
               o.length >= 0 &&                        
               o.length === Math.floor(o.length) &&    
               o.length < 4294967296) 
            return true
        else
            return false
    }
    for(let el of arr){
        res[k++] = el
    }
    for(let el of args){
        // 如果是数组且没有禁止展开
        if(Array.isArray(el) && el[Symbol.isConcatSpreadable] != false){
            for(let _el of el){
                res[k++] = _el
            }
        } else {
            // 如果是类数组且允许展开
            if(isArrayLike(el) && el[Symbol.isConcatSpreadable]){
                for(let key in el){
                    // 把除了 length 之外的键值都放入新数组中
                    if(key !== 'length'){
                        res[k++] = el[key]
                    }
                }
            } else {
                res[k++] = y
            }
        }
    }
    return res
}

PS:这里检测类数组对象的方式可能不太严谨,且没有考虑 empty 元素的情况

at

用法

at 是一个比较新的方法,目前浏览器还没有实现:

  • 该方法接受一个整数作为参数,并返回数组对应索引的元素。
  • 如果参数是负数且绝对值小于数组长度,则将其与数组长度相加作为需要查找的索引。
  • 如果没有符合索引的元素,则返回 undefined

相比 arr[2],这个方法的优势在哪里呢?优势在于可以很方便地访问那些数组末尾的元素,比如现在要访问 const arr = [1,2,3,4] 的倒数第二个元素,不再需要使用 arr[arr.length - 2],只需要 arr.at(-2)

const arr = [1,2,3,4]
arr.at(2)   // 3
arr.at(-1)  // 4

实现

Array.prototype.myAt = function(searchIndex){
    let arr = this
    let len = arr.length
    let searchIndex = searchIndex >= 0 ? 
        searchIndex : Math.abs(searchIndex) < len ? searchIndex + len : Infinity
    return arr[searchIndex]
}

indexOf

用法

  • 接受两个参数,第一个参数表示查找目标,第二个参数表示开始查找位置
  • 第二个参数可以是正数或者负数,正数超出数组索引直接返回 -1,负数与数组长度相加后若是正数则作为开始查找位置,若是负数则从 0 开始查找
  • 找到就返回元素索引,否则返回 -1
  • 采用严格相等去匹配数组元素
const arr = ['a','b','c','d','a','e']

arr.indexOf('b')           // 从前往后查找'b',返回它的索引1
arr,indexOf('b',2)         // 从索引2开始,从前往后查找'b',找不到,返回 -1

arr.lastIndexOf('a')       // 从后往前查找'a',返回它的索引4
arr.lastIndexOf('a',2)     // 从索引2开始,从后往前查找'a',返回它的索引0

arr.includes('c')          // 数组存在'c',返回 true
arr.includes('c',3)        // 从索引3开始,数组不存在'c',返回 false 
arr.includes('c',300)      // 超出数组长度,返回 false
arr.includes('c',-2)       // 负值=>负值+数组长度=>4,从索引4开始查找,返回 false
arr.includes('c',-100)     // 负值=>负值+数组长度=>-94,从头开始查找,返回 true

实现

Array.prototype.myIndexOf = function(target,start = 0){
    let arr = this
    let len = arr.length
    let _start = start >= 0 ? start : Math.abs(start)<= len ? len + start : 0
    for(;_start < len;_start++){
        if(arr[_start] === target){
            return _start
        }
    }
    return -1
}

lastIndexOf

用法

lastIndexOf 和 indexOf 相比,有些地方是反过来的:

  • 一直都是从后往前查找
  • 第二个参数可以是正数或者负数,正数超出数组索引则从最末尾开始查找,负数与数组长度相加后若是正数则作为开始查找位置,若是负数则直接返回 -1
const arr = [1,2,3,2,5]
arr.lastIndexof(2)    // 3

实现

Array.prototype.myLastIndexOf = function(target,start){
    let arr = this
    let len = arr.length 
    start = start || arr[arr.length - 1]
    let _start = start < 0 ? len + start : start >= len ? arr.length - 1 : start
    for(;_start > 0;_start--){
        if(arr[_start] === target){
            return _start
        }
    }
    return -1
}

includes

用法

inlcudes 和 indexOf 类似,但是返回的是布尔值。

为什么有了 indexOf 还要引入 inlcudes?一是因为返回布尔值,语义更加清晰;二是因为 includes 内部使用的是类似 Object.is 的比较方式,而非 indexOf 所使用的 ===,所以可以准确判断 NaN。

[1,NaN].indexOf(NaN)     // -1
[1,NaN],includes(NaN)    // true

// 然而,inlcudes 仍然无法准确判断±0,会认为两者相等
[1,+0].includes(-0)      // true
[1,0].includes(+0)       // true 

实现

Array.prototype.myIncludes = function(target,start = 0){
    let arr = this
    let len = arr.length
    let _start = start >=0 ? start : Math.abs(start) <= len ? start + len : 0
    function isSame(x,y){
        return x === y || typeof(x)=='number'&&typeof(y)=='number'&&isNaN(x)&&isNaN(y) 
        // return x === y || x!=x && y!= y
        // return x === y || Number.isNaN(x) && Number.isNaN(y)
    }
    for(;_start < len;_start++){
        if(isSame(arr[_start],target)){
            return true
        }
    }
    return false
}

这里判断 NaN 的方式很多,一种是直接利用最准确的 Number.isNaN,一种是使用 isNaN,但要保证参数是数字,还有一种是利用 NaN 自身的特性,即“自己不等于自己”。

slice

用法

slice 用于产生数组切片:

  • 可以接受两个参数 beginend,表示开始位置和结束位置;可以只接受一个参数 begin,表示开始位置;可以不接受任何参数,则缺省开始位置为第一个元素,结束位置为最后一个元素
  • begin 可以是正数或者负数:

    • 如果是正数,直接取自身;
    • 如果是负数,且负数绝对值不超过数组长度,则将其与数组长度相加,若超过数组长度,则取 0
  • end 可以是正数或者负数:

    • 如果是正数,且不超过数组长度,则取自身,否则取数组长度;
    • 如果是负数,且负数绝对值不超过数组长度,则将其与数组长度相加
  • 在上面规则的作用下,begin 可能大于 end,此时就直接返回一个空数组
const arr = [1,2,3,4,5]
arr.slice(1)           // [2,3,4,5]
arr.slice(1,4)         // [2,3,4]
arr.slice(-4,-1)       // [2,3,4]     负值 => 数组长度加负值
arr.slice(4,1)         // []          反向索引,返回空数组

实现

// 通过默认参数值,为 begin 和 end 设置缺省值
Array.prototype.mySlice = function(begin = 0,end = this.length){
    let arr = this
    let len = arr.length
    let res = []
    let k = 0
    begin = begin >= 0 ? begin : Math.abs(begin) <= len ? begin + len : 0
    end = end < 0 ? end + len : Math.min(end,len)
    for(;begin < end;begin++){
           res[k++] = arr[begin]
    }
    return res
}

flat

用法

用于数组扁平化(数组降维):

  • 传入的参数代表对于数组中的每一个元素,要降维多少次,默认为 1 次,传入 Infinity 可以直接将数组转化为一维数组
  • flat 本身会跳过 empty 元素,因此这里遍历数组的时候需要进行检查。要么是使用前面那样的 for循环 + in 手动检查 empty 元素,要么是使用本身就可以跳过 empty 元素的数组遍历方法(比如 reduce 或者 forEach 等)
  • flat 的实现可以参考数组扁平化的方法,但它实现起来需要更加灵活,可以传参控制降维次数
[1,[2,3],[[4,5],6]].flat()          // [1,2,3,[4,5],6]
[1,[2,3],[[4,5],6]].flat(2)          // [1,2,3,4,5,6]

实现

1)reduce + 递归

Array.prototype.myFlat = function(times = 1){
    let arr = this
    // 如果参数无法转化为数字,或小于等于0,则直接将原数组返回
    if(!Number(times) || Number(times) <= 0){
        return arr
    }
    return arr.reduce((acc,cur) => {
        return acc.concat(Array.isArray(cur) ? cur.myFlat(times - 1) : cur)
    },[])
}

2)forEach + 递归

 Array.prototype.myFlat = function(times = 1){
    let arr = this
    let res = []
    if(!Number(times) || Number(times) <= 0){
        return arr
    }
    arr.forEach(el => {
        res.concat(Array.isArray(el) ? el.myFlat(times - 1) : el)
    })
    return res
}

3)for 循环 + in 检查 + 递归

Array.prototype.myFlat = function(times = 1){
    let arr = this
    let res = []
    if(!Number(times) || Number(times) <= 0){
        return arr
    }
    for(let i = 0;i < arr.length;i++){
        if(i in arr){
            if(Array.isArray(arr[i])){
                   res = [...res,...arr[i].myFlat(times - 1)]
            } else {
                res = [...res,arr[i]]
            }
        }
    }
    return res
}

用于遍历的方法

forEach

用法

可以遍历数组的每个元素,执行特定的操作:

  • 接受两个参数,一个回调函数和一个缺省为 null 的 thisArg。会遍历数组中所有元素,对每个元素执行该函数;
  • 不会新建数组也不会修改原数组,总是返回 undefined(即使明确指定了返回值)
  • 遍历范围在执行回调前就确定了,遍历过程中 push 会修改原数组,但是不会影响遍历范围
  • 遍历过程中可以提前修改后面才会遍历到的元素
  • 会直接跳过 empty 元素
// res 为 undefined
const res = ['a','b','c'].forEach(function(item,index,arr){
    console.log(`${index}-${item}`,arr,this)
    return 123
},{})                                                        
// 遍历过程中即使push了新元素,也仍然按照原数组长度进行遍历。打印 1,2
[1,2].forEach((item,index,arr) => {
    arr.push(3,4,5)
    console.log(item)
})
// 遍历过程中可以提前修改未遍历元素。打印 1,100
[1,2].forEach((item,index,arr) => {
    arr[1] = '100'
    console.log(item)
})
// 遍历过程中可以 return,但只是结束当前这次遍历,无法跳出整个 forEach。打印 2
[1,2].forEach((item,index,arr) => {
    if(index == 0) return
    console.log(item)
})
// 遍历过程中会自动跳过 empty 元素(null 和 undefined 不会跳过)。打印 1,2,4
[1,2,,4].forEach((item,index,arr) => {    
    console.log(item)
})

实现

Array.prototype.myforEach = function (fn,thisArg = null){
    if(typeof fn != 'function'){
        throw new TypeError(`${fn} is not a function`)
    }
    let arr = this    
    for(let i = 0,len = arr.length;i < len;i++){
        // 如果不是 empty 元素
        if(i in arr){
            fn.call(thisArg,arr[i],i,arr)
        }
    }
}

map

用法

将原数组的每个元素映射为执行回调函数之后的返回值

  • 基本实现和 forEach 差不多,也是会跳过 empty 元素
  • forEach 的遍历范围一开始就确定好,所以需要先保存最初数组长度;同理,map 最终返回的新数组长度也是一开始就与原数组长度绑定好了,声明新数组的时候需要指定这个长度
// 返回新数组 [2,4,6,8]
[1,2,3,4].map((item,index) => item * 2)

实现

Array.prototype.myMap = function(fn,thisArg = null){
    if(typeof fn != 'function'){
        throw TypeError(`${fn} is not a function`)
    }
    let arr = this
    // 这里不要使用 let newArr = [],否则修改原数组长度时会影响新数组长度
    let newArr = new Array(arr.length)
    for(let i = 0,len = arr.length;i < len;i++){
        if(i in arr){
            const res = fn.call(thisArg,arr[i],i,arr)
            newArr[i] = res
        }
    }
    return newArr
}

flatMap

用法

flatMap 相当于是 mapflat(1) 的结合。它会给某个数组调用 map 方法,如果得到了一个多维数组,则会对该数组进行一次降维

PS:注意这个方法不会改变原数组。

const arr1 = [1, 2, 3, 4];

arr1.map(x => [x * 2]);
// [[2], [4], [6], [8]]

arr1.flatMap(x => [x * 2]);
// [2, 4, 6, 8]

实现

我们可以在每次执行 flatMap 的回调并返回一个新结果时,判断该结果是不是数组,如果是则取出数组中的每个元素放入最终返回的新数组中。

Array.prototype.myFlatMap = function(fn,thisArg = null){
    if(typeof fn != 'function'){
        throw new TypeError(`${fn} is not a function`)
    }
    let arr = this
    let newArr = new Array(arr.length)
    let k = 0
    for(let i = 0;i < arr.length;i++){
        if(i in arr){
            const res = fn.call(thisArg,arr[i],i,arr)
            if(Array.isArray(res)){
                for(let el of res){
                    newArr[k++] = el
                }
            } else {
                newArr[k++] = res
            }
        }
    }   
    return newArr
}

find

用法

find 返回数组中第一个符合条件的元素或者 undefined:

  • 通过indexOf 搜索数组,无法自定义搜索条件,所以出现了 find
  • find 会对每个元素执行一次回调函数,直到找到符合条件的元素,就将这个元素返回(永远只返回一个),并结束函数执行;找不到则返回 undefined
  • 注意这个方法不会跳过 empty 元素,所以这里不做 i in arr 的检查
[1,2,3,4,5].find((item,index,arr) => item > 2)      // 3

实现

Array.prototype.myFind = function(fn,thisArg = null){
    if(typeof fn != 'function'){
        throw new TypeError(`${fn} is not a function`)
    }
    let arr = this
    for(let i = 0;i < arr.length;i++){
        const result = fn.call(thisArg,arr[i],i,arr)
        if(result){
            return arr[i]
        }
    }
    return undefined
}

findIndex

用法

find 基本一致,但是 findIndex 返回的是第一个符合条件的元素的索引,没有这样的元素就返回 -1

[1,2,3,4,5].findIndex((item,index) => item > 2)      // 2

实现

Array.prototype.myfindIndex = function(fn,thisArg = null){
    if(typeof fn != 'function'){
        throw new TypeError(`${fn} is not a function`)
    }
    let arr = this
    for(let i = 0;i < arr.length;i++){
        const result = fn.call(thisArg,arr[i],i,arr)
        if(result){
            return i
        }
    }
    return -1
}

filter

用法

通过 find 搜索数组,只能找到一个符合条件的元素,而 filter 可以筛选出所有符合条件的元素:

[1,2,3,4,5].filter((item,index) => item > 2)        // 返回 [3,4,5]
[1,2,3,4,5].filter((item,index) => item > 100)      // 没有符合的元素,返回 []

实现

Array.prototype.myFilter = function(fn,thisArg = null){
    if(typeof fn != 'function'){
        throw new TypeError(`${fn} is not a function`)
    }
    let arr = this
    let res = []
    let k = 0
    for(let i = 0;i < arr.length;i++){
        if(i in arr){
            const result = fn.call(thisArg,arr[i],i,arr)
            // 如果元素符合条件,则放入新数组中
            if(result){
                res[k++] = arr[i]
            }
        }
    }
}

some

用法

接受一个回调函数表示判断条件,只要数组中有一个元素满足该条件(回调函数返回 true),some 方法就返回 true,否则返回 false

[1,2,3,4].some((item,index) => item>3)      // 至少有一个大于3的数,返回 true
[1,2,3,4].some((item,index) => item>100)    // 没有一个大于100的数,返回 false

实现

Array.prototype.mySome = function(fn,thisArg = null){
    if(typeof fn != 'function'){
        throw new TypeError(`${fn} is not a function`)
    }
    let arr = this
    for(let i = 0;i < arr.length;i++){
        const result = fn.call(thisArg,arr[i],i,arr)
        if(result){
            return true
        }
    }
    return false
}

every

用法

接受一个回调函数表示判断条件,只有数组中所有元素都满足该条件(回调函数返回 true),every 方法才会返回 true,有一个不满足都会返回 false

[1,2,3,4].every((item,index) => item>0)     // 所有元素都大于0,返回 true
[1,2,3,4].every((item,index) => item>3)     // 并非所有元素都大于3,返回 false

实现

Array.prototype.myEvery = function(fn,thisArg = null){
    if(typeof fn != 'function'){
        throw new TypeError(`${fn} is not a function`)
    }
    let arr = this
    for(let i = 0;i < arr.length;i++){
        const result = fn.call(thisArg,arr[i],i,arr)
        if(!result){
            return false
        }
    }
    return true
}

reduce

用法

reduce 可以归并数组的每个元素,最终构建一个累计归并值作为返回值:

  • 语法:arr.reduce((acc,cur,index,arr) => {...},baseAcc)
  • 接受两个参数,一个是回调函数,一个是初始累计归并值。其中,回调的当前返回值会作为下次迭代所使用的 acc,也即累计归并值。初始累计归并值缺省是第一个非 empty 元素,且此时会从该元素的下一个元素开始迭代。
// 没有提供初始累计归并值,因此缺省是1,并且从2开始迭代
[1,2,3,4].reduce((acc,cur) => acc + cur)              // 10

// 提供100作为初始累计归并值,从1开始迭代
[1,2,3,4].reduce((acc,cur) => acc + cur,100)          // 110

// 二维数组转化为一维数组,concat 本身会拍平一维数组
[1,2,[3,4]].reduce((acc,cur) => acc.concat(cur),[])       // [1,2,3,4]

实现

实现的时候,有两个关键的地方:

  • 如何判断有没有传第二个参数?用 typeof baseAcc === 'undefined' 判断不准确,因为有可能传的第二个参数确实就是 undefined,这里可以通过剩余参数的长度判断
  • 如何找出数组的第一个非 empty 元素?遍历数组,用 in 判断
Array.prototype.myReduce = function(...args){
    let fn = args[0]
    let arr = this
    let len = arr.length
    let index = 0,acc
    if(typeof fn != 'function'){
        throw new TypeError(`${fn} is not a function`)
    }
    // 如果传了第二个参数
    if(args.length >= 2){
        acc = args[1]
    } else {
        // 只要当前数组还没找到非 empty 元素,就一直遍历下去
        while(index < len && !(index in arr)){
            index++
        }
        // 如果数组是一个充满 empty 元素的空数组,则抛出错误
        if(index >= len){
            throw new TypeError('Reduce of empty array with no initial value')
        }
        // index 加一,表示第一个非 empty 元素的下一个元素
        acc = arr[index++]
        for(;index < len;index++){
            if(index in arr){
                acc = fn(acc,arr[index],index,arr)
            }
        }
        return acc
    }    
}

reduceRight

用法

用法基本和 reduce 一致,区别是它是从后往前去遍历数组的。

[0, 1, 2, 3].reduceRight((acc, cur) => {
  console.log(cur);
});
// 2
// 1
// 0

实现

reduceRight 的实现和 reduce 基本一样,但需要注意:非 empty 元素的查找以及数组的遍历顺序是反过来的

Array.prototype.myReduceRight = function(...args){
    let fn = args[0]
    let arr = this
    let len = arr.length
    let index = len - 1,acc
    if(typeof fn != 'function'){
        throw new TypeError(`${fn} is not function`)
    }
    if(args.length >= 2){
        acc = args[1]
    } else {
        while(index > 0 && !(index in arr)){
            index--
        }
        if(index == 0){
            throw new TypeError('Reduce of empty array with no initical value')
        }
        acc = arr[index--]
        for(;index >= 0;index--){
            if(index in arr){
                acc = fn(acc,arr[index],index,arr)
            }
        }
        return acc
    }
}

静态方法

Array.isArray

用法

判断传入的参数是不是数组

Array.isArray([1,2,3])   // true

实现

这里可以直接借用 Object.prototype.toString 判断数据类型

Object.defineProperty(Array,"myIsArray",{
    value: function(arr){        
        return Object.prototype.toString.call(arr) === '[object Array]'
    }
})

Array.from

用法

基于一个传进来的类数组对象或者可迭代对象,浅拷贝生成一个新数组。如果指定了第二个参数为回调函数,则会为新数组的每个元素执行一次该回调函数

const set = new Set(['foo', 'bar', 'baz', 'foo']);
Array.from(set);
// [ "foo", "bar", "baz" ]

实现

Object.defineProperty(Array,"myFrom",{
    value:function(toChange,fn,thisArg = null){
        let res = [...toChange]
        if(typeof fn === 'function'){
            for(let i = 0;i < res.length;i++){
                res[i] = fn.call(thisArg,res[i],i,res)
            }
        }
        return res
    }
})

Array.of

用法

接受多个参数,所有参数都会成为新创建的数组的元素

Array.of(7);       // [7]
Array.of(1, 2, 3); // [1, 2, 3]

实现

Object.defineProperty(Array,"myOf",{
    value: function(){
        let res = []
        for(let i = 0;i < arguments.length;i++){
            res[i] = arguments[i]
        }
        return res
    }
})                                            

以上就是本文的全部内容,感谢阅读。


本文转自网络,版权归原作者所有,原文链接:https://segmentfault.com/a/1190000040138580
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文


随机推荐