JS 陣列 API 原始碼淺析

方應杭在飢人谷發表於2018-03-01

這是《JS 深入淺出》第八課的部分講義

今天的課的主要內容有

  1. 陣列的七個 API 的實現
  2. Underscore 原始碼如何讀(函式節流防抖、陣列去重都能在原始碼裡看到)

join

先從最簡單的陣列操作開始:

var array = ['a','b','c']
array.join('-') // 結果是 'a-b-c'
複製程式碼

我們畫一下記憶體圖:

記憶體分圖

  1. array.join 實際上是 Array.prototype.join 對應的函式(array.join === Array.prototype.join === ADDR401)
  2. array.join('-') 等價與 array.join.call(array, '-')
  3. join 函式通過 this 和 arguments[0] 可以得到 array 和 '-' 兩個值

所以我們可以大膽猜測 array.join 的原始碼大概是這樣的

Array.prototype.join = function(char){
  let result = this[0] || ''
  let length = this.length
  for(let i=1; i< length; i++){
      result += char + this[i]
  }
  return result
}
複製程式碼

this 就是 array,因為你使用 array.join('-') 來呼叫 join 的(隱式指定this)

slice

接下來研究第二個陣列操作

array.slice(beginIndex, endIndex)
複製程式碼

顯而易猜,原始碼大概大概大概是這樣的

Array.prototype.slice = function(begin, end){
    let result = []
    begin = begin || 0
    end = end || this.length
    for(let i = begin; i< end; i++){
        result.push(this[i])
    }
    return result
}
複製程式碼

於是很多前端用 slice 來將偽陣列,轉化成陣列

array = Array.prototye.slice.call(arrayLike)
或者
array = [].slice.call(arrayLike)
複製程式碼

ES 6 看不下去這種蹩腳的轉化方法,出了一個新的 API

array = Array.from(arrayLike)
複製程式碼

專門用來將偽陣列轉化成真陣列。

P.S. 偽陣列與真陣列的區別就是:偽陣列的原型鏈中沒有 Array.prototype,而真陣列的原型鏈中有 Array.prototype。因此偽陣列沒有 pop、join 等屬性。

sort

我聽說大部分的語言內建的 sort 方法都是快速排序演算法。我們就簡化成選擇排序吧

Array.prototype.sort = function(fn){
    fn = fn || (a,b)=> a-b
    let roundCount = this.length - 1 // 比較的輪數
    for(let i = 0; i < roundCount; i++){
        let minIndex = this[i]
        for(let k = i+1; k < this.length; k++){
            if( fn.call(null, this[k],this[i]) < 0 ){
                [ this[i], this[k] ] = [ this[k], this[i] ]
            }
        }
    }
}
複製程式碼

fn.call(null, this[k], this[i]) 決定了第 k 項和第 i 項的前後(大小)關係。

fn 為 (a,b) => a-b 表示啥?fn 為 (a,b) => b-a 又表示啥?

不重要,因為如果前者不符合你的需求,那麼後者一定符合你的需求,你只需要試兩邊就知道用哪一個了。

forEach、 map、filter 和 reduce

Array.prototype.forEach = function(fn){
    for(let i=0;i<this.length; i++){
        if(i in this){
            fn.call(undefined, this[i], i, this)
        }
    }
}
複製程式碼

forEach 和 for 的區別主要有兩個:

  1. forEach 沒法 break
  2. forEach 用到了函式,所以每次迭代都會有一個新的函式作用域;而 for 迴圈只有一個作用域(著名前端面試題就是考察了這個)
Array.prototype.map = function(fn){
    let result = []
    for(let i=0;i<this.length; i++){
        if(i in this) {
            result[i] = fn.call(undefined, this[i], i, this)
        }
    }
    return result
}
複製程式碼

map 和 forEach 功能差不多,區別只有返回值而已。 接下來是 filter

Arra.prototype.filter = function(fn){
    let result = []
    let temp
    for(let i=0;i<this.length; i++){
        if(i in this) {
            if(temp = fn.call(undefined, this[i], i, this) ){
                result.push(this[i])
            }
        }
    }
    return result
}
複製程式碼

fn.call() 返回真值就 push 到返回值,沒返回真值就不 push。 接下來是 reduce

Arra.prototype.reduce = function(fn, init){
    let result = init
    for(let i=0;i<this.length; i++){
        if(i in this) {
            result = fn.call(undefined, result, this[i], i, this)
        }
    }
    return result
}
複製程式碼

map、filter 和 reduce 的區別:

區別

map、filter 和 reduce 的聯絡:

  1. map 可以用 reduce 表示
    array2 = array.map( (v) => v+1 )
    可以寫成 
    array2 = array.reduce( (result, v)=> {
        result.push(v + 1)
        return result
    }, [ ] )
    複製程式碼
  2. filter 可以用 reduce 表示
    array2 = array.filter( (v) => v % 2 === 0 )
    可以寫成
    array2 = array.reduce( (result, v)=> {
        if(v % 2 === 0){ result.push(v) }
        return result
    }, [])
    複製程式碼

Underscore.js

Underscore 是一個集合操作的庫(當時 JS 沒有 Set,所以集合指的是陣列和物件) 主要有六類 API:

  1. 集合 API
  2. 陣列 API
  3. 物件 API
  4. 函式 API
  5. 雜項 API
  6. 鏈式操作

Underscore 原始碼閱讀建議

  1. 搜尋 underscore annotated source code,點進去
  2. 搜尋自己感興趣的 API,如 _.uniq
  3. 邊看文件邊看程式碼

後面講義需收費觀看。課後還有兩道經典面試題。

歡迎購買《JS 深入淺出》。

相關文章