JS 數組真強大

其實 JS 的數組可以玩的很花,但是很多人沒有發現(不管你會不會,在我面前都屬於不瞭解)

先來看5個簡單的 api

const a = []
a.push(1)
// 1W
a.push(2)
// 2
a.push(100, 200)
// 4
a.pop()
// 200
a.pop()
// 100
a.pop()
// 2
a.pop()
// 1
a.pop()
 // undefined 此時就能發現 JS 有點傻了
a.push(undefined); a.pop()
// undefined 這麼一來就分不清了,這個 undefined 是數組裏面的,還是彈出來的
a.unshift(100)
// 1
a.unshift(200)
// 2 這個 api 和 push 很像,只不過是從前面塞進去
a.shift()
// 200
a.shift()
// 100
a.shift()
// undefined

應用一

翻轉字符串

'abcdef'.split().reverse().join()

應用二

發佈訂閱

const eventBus = {
  on() {}, // addEventListener
  emit() {}, // trigger
  off() {}
}

eventBus.on('click', (data) => { console.log(`click: ${data}`) })

setTimeout(() => { // 這裏一般是用戶觸發,我這先暫時用定時器模擬
  eventBus.emit('click', '來自 emit click 的數據')
}, 2000)

這就是一個最小的發佈訂閱模式,現在要做的就是把上面的函數補全

這種東西跟數組有什麼關係呢?實際上呢,如果你學過數據結構,你就知道這種發佈訂閱就是把訂閱的函數放到一個數組裏就好了

在想一個函數的時候(不管是封裝組件或者是其他任何東西的時候,你都要想好參數是什麼,返回值是什麼),當然我們現在不設計返回值

const eventBus = {
  events: {}, // 這裏爲什麼是一個對象呢,有可能會是 { click: [], change: [], ... }
  on(eventName, fn) {
    const events = this.events[eventName]
    events.push(fn)
    
    // events 默認可能爲空,馬上優化一下
    // if (!this.events[eventName]) this.events[eventName] = []
    this.events[eventName] = this.events[eventName] || []
    this.events[eventName].push(fn)
  },
  emit(eventName, data) {
    const events = this.enents[eventName]
    for(let i = 0;i< events.length;i++){ // 暫時不用 map,foreach
      const fn = events[i]
      fn()
    }

    // 可以看到所有的複雜代碼都是通過 ifelse for 循環來實現的,其他高級的東西都可以通過這兩個來實現

    // 可能 events 爲空,所以還要加上判斷
    const events = this.events[eventName]
    if (!events) return // 防禦式編程
    for(let i = 0;i< events.length;i++){
      events[i](data)
    }
  },
  off() {}
}
eventBus.on('click', (data) => { console.log(`click: ${data}`) })

eventBus.emit('click', '來自 emit click 的數據')

上述其實我們經常寫,就像我們監聽瀏覽器中的事件一樣,button.addEventListener(e => {})
這個 e 哪來的,就是上面 emit 的傳的,可以用戶觸發,也可以有 button.trigger('click', {})
所以所有 dom 元素都自帶發佈訂閱,或者說所有 dom 都繼承發佈訂閱接口
但是上述還有取消監聽 off,沒有寫,其實很簡單,只需要從 events 裏面刪除事件就好了

接下來如何從數組裏面刪除一個元素?

const a = []

a.splice(0, 0, 1)
// []

a.splice(1, 0, 2)
// []

a
// [1, 2]

a.splice(1, 1)
// [2]

a.splice(0, 0, 3)
// []

a
// [3, 1]

a.splice(0, 1)
// [3]

a
// [1]

splice 可以在任何位置增和刪,相當於上面四個 api(unshift、shift、push、pop)

再來看數組的另外7個 api

join&slice&sort

array.join('-')
用於將數組所有元素連接成一個字符串並返回這個字符串。

// 大膽猜測一下源碼,手動實現一個 join
Array.prototype.myJoin = function(char){
  let result = this[0] || ''
  let length = this.length
  for(let i=1; i< length; i++){
      result += char + this[i]
  }
  return result
}

array.slice(beginIndex, endIndex)
用下標切割一個數組並返回一個新的數組對象,原始數組不會被改變。

// 大膽猜測一下源碼,手動實現一個 slice
Array.prototype.mySlice = 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 來做僞數組轉換
因爲 slice 會用 for 循環遍歷然後生成一個新數組,只需要原來的數據有個 length 屬性就夠了

array = Array.prototype.slice.call(fakeArray)
或者
array = [].slice.call(fakeArray)

ES6 看不下去這種蹩腳的轉換方法,出了一個新的 api

array = Array.from(fakeArray)

sort((a, b) => a - b),接受的函數可傳可不傳
用來排序一個數組,據說大部分語言的 sort 都是用的快排,這裏先簡化成選擇排序把(每次都選擇最小的放在前面,第二次選擇第二小的放在第二個,第三次選擇第三小的放在第三個......,以此類推)

// 這是一個很慢的算法(On2)
Array.prototype.mySort = 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] ] // ES6 互換位置
            }
        }
    }
}

然後在說說上面的參數,如果想從小到大排序,到底是 (a, b) => a - b 還是 (a, b) => b - a 呢,怎麼記憶呢
答案是不需要記憶,試兩次就好了,[2, 3, 1].sort((a, b) => a - b)[2, 3, 1].sort((a, b) => b - a)

forEach、 map、filter 和 reduce

forEach

Array.prototype.myForEach = function(fn){
    for(let i=0;i<this.length; i++){
        if(i in this){ // 注意此處有可能會是 empty,所以需要加判斷
            fn.call(undefined, this[i], i, this) // 這裏的 this 先用 undefined 簡化,其實原forEach支持改變this
        }
    }
}

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

  1. forEach 沒法 break
  2. forEach 用到了函數,所以每次迭代都會有一個新的函數作用域;而 for 循環只有一個作用域(著名前端面試題就是考察了這個)舉例

map

Array.prototype.myMap = 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 功能差不多,區別只有返回值而已,所以我推薦忘掉 forEach,只用 map 即可(名字又短,還有返回值)。
想用 map 的返回值就用,不用想就放在一邊。
那些在用 forEach 無非是不會 map,或者 forEach 名字比較直觀

filter

Array.prototype.myFilter = 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]) // fn.call() 返回真值就 push 到返回值,沒返回真值就不 push。
            }
        }
    }
    return result
}

reduce
講了這麼多,就是爲了最後講她,代碼其實很簡單,可能思考起來比較難
簡單來說他是一個累加器,遍歷的時候能把上一次的結果和這次進行操作,然後返回
舉個簡單例子 [1,2,3,4,5].reduce((result, item) => result + item, 0),輸出 15

Array.prototype.myReduce = 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) // 這個 result 至關重要
        }
    }
    return result
}

通過我們實現的源碼來看,好像和之前幾個 api 差不多,只是有個 result 的區別

其實正是這樣,先來看看他們之前的聯繫

// 之前說 forEach 可以用 map 表示
// 現在 map 可以用 reduce 表示
array2 = array.map( (v) => v+1 )
// 可以寫成 
array2 = array.reduce( (result, v)=> {
     result.push(v + 1)
     return result
}, [ ])
// filter 可以用 reduce 表示
array2 = array.filter( (v) => v % 2 === 0 )
// 可以寫成
array2 = array.reduce( (result, v)=> {
     if(v % 2 === 0){ result.push(v) }
     return result
}, [])

也就是說 reduce 是最核心的 api,只要搞清楚他,其他的都能表示(都能弄明白)
基本上這裏所有的 api,都能夠用 reduce 表示出來
拓展Transducers知乎中文

應用三

LazyMan
// 實現一個LazyMan,可以按照以下方式調用:
LazyMan("Hank")
// 輸出:
Hi! This is Hank!

LazyMan("Hank").sleep(10).eat("dinner")
// 輸出
Hi! This is Hank!
//等待10秒..
Wake up after 10
Eat dinner~

LazyMan("Hank").eat("dinner").eat("supper")
// 輸出
Hi This is Hank!
Eat dinner~
Eat supper~

LazyMan("Hank").sleepFirst(5).eat("supper")
// 輸出
// 等待5秒
Wake up after 5
Hi This is Hank!
Eat supper
// 以此類推。

首先第一題簡單

function LazyMan (name) {
 console.log(`Hi! This is ${name}!`)   
}

第二題也簡單

// 先實現,不要在意這些細節
function LazyMan (name) {
    console.log(`Hi! This is ${name}!`)
    return {
       sleep() {
           setTimeout(() => {
               console.log('Wake up after 10')
           }, 3000) // 爲了方便調試,暫時縮短時間
           return {
               eat() {
                   setTimeout(() => {
                        console.log('Eat dinner~')
                   }, 3000)
               }
           }
       }   
    }
}

第三題,稍微優化一下代碼,實現一個鏈式調用

function LazyMan (name) {
    console.log(`Hi! This is ${name}!`)
    const api = {
       sleep() {
           setTimeout(() => {
               console.log('Wake up after 10')
           }, 3000) // 爲了方便調試,暫時縮短時間
           return api
       }, 
       eat() {
           setTimeout(() => {
                console.log('Eat dinner~')
           }, 3000)
           return api
       }
    }
    return api
}

第四題,也是最難的一題,因爲下來的 sleepFirst 要在所有函數前執行,目前根本做不到,所以現在的代碼要推倒重來(就像產品經理說我們要做一個很像百度的需求,前面的需求都很簡單,最後突然插入說我們要在前面加一個搜索框就行了,能搜索產品內的任何東西,這時開發就傻了,你怎麼不一開始就說做一個百度或淘寶,前面的需求這麼簡單,後面成噸的需求砸過來),所以要用隊列上場了

分析一下問題:我們拿到函數以後不能立馬執行,需要到某個時候才能做?
很像上面的發佈訂閱把
因爲 sleepFirst 在後面調用,所以不能常規執行函數,我們需要一個任務隊列(不叫數組),纔可以讓 sleepFirst 插隊

// 先收集所有的任務
function LazyMan (name) {
    const array = []
    const fn = () => {
        console.log(`Hi! This is ${name}!`)
    }
    array.push(fn)
    const api = {
       sleep() {
           array.push(() => {
               setTimeout(() => {
                   console.log('Wake up after 10')
               }, 3000)
           })
           return api
       }, 
       eat() {
           array.push(() => {
               console.log('Eat dinner~')
           })
           return api
       },
       sleepFirst() {
           array.unshift(() => {
                setTimeout(() => {
                   console.log('Wake up after 5')
               }, 3000)
           })
           return api
       }
    }
    setTimeout(() => array.map(v => v())) // 等收集到所有任務以後,開始執行函數
    return api
}

這樣一來我們就改寫全部改寫了原來的代碼,並依次執行,但是還存在一個問題,雖然函數是按照我們的順序排列了,但是因爲異步導致輸出並不是我們想要的結果

之所以叫任務隊列,而不是叫做數組,是因爲還是和數組有點區別的,函數具體的執行應該由上一個任務主動呼叫的

所以需要實現一個 next 函數,來手動來通知下一個函數的執行(有點像上面的 emit)

const next = () => {
    const fn = array.shift()
    fn && fn()
}
const api = {
   sleep() {
       array.push(() => {
           setTimeout(() => {
               console.log('Wake up after 10')
               next() // 每個函數執行以後,都需要調用 next,通知下一個任務可以開始執行了
           }, 3000)
       })
       return api
   },
   // ....
}
setTimeout(() => next())
function LazyMan (name) {
    const array = []
    const fn = () => {
        console.log(`Hi! This is ${name}!`)
        next()
    }
    array.push(fn)
    const next = () => {
        const fn = array.shift()
        fn && fn()
    }
    const api = {
       sleep() {
           array.push(() => {
               setTimeout(() => {
                   console.log('Wake up after 10')
                   next()
               }, 3000)
           })
           return api
       }, 
       eat() {
           array.push(() => {
               console.log('Eat dinner~')
               next()
           })
           return api
       },
       sleepFirst() {
           array.unshift(() => {
                setTimeout(() => {
                   console.log('Wake up after 5')
                   next()
               }, 3000)
           })
           return api
       }
    }
    setTimeout(() => next())
    return api
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章