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
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章