JS 原生方法原理探究(六):手寫實現 30 個數組原生 API

這是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 方法能夠往數組末尾添加任意多個元素,並將數組長度做爲返回值:this

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 方法能夠往數組頭部添加任意多個元素,並將數組長度做爲返回值:spa

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 將原數組進行反轉,最終返回原數組prototype

[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 方法。code

  • 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 ,以及添加的元素 item一、item二、...
  • 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
    }
})

以上就是本文的所有內容,感謝閱讀。

相關文章
相關標籤/搜索