其实 JS 的数组可以玩的很花,但是很多人没有发现(不管你会不会,在我面前都属于不了解)
先来看5个简单的 api 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const a = [] a.push(1 ) // 1 Wa.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
应用一 翻转字符串
1 'abcdef' .split ().reverse ().join ()
应用二 发布订阅
1 2 3 4 5 6 7 8 9 10 11 const eventBus = { on() {}, // addEventListener emit() {}, // trigger off() {} } eventBus.on('click', (data) => { console.log(`click: ${data}`) }) setTimeout(() => { // 这里一般是用户触发,我这先暂时用定时器模拟 eventBus.emit('click', '来自 emit click 的数据') }, 2000)
这就是一个最小的发布订阅模式,现在要做的就是把上面的函数补全
这种东西跟数组有什么关系呢?实际上呢,如果你学过数据结构,你就知道这种发布订阅就是把订阅的函数放到一个数组里就好了
在想一个函数的时候(不管是封装组件或者是其他任何东西的时候,你都要想好参数是什么,返回值是什么),当然我们现在不设计返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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() {} }
1 2 3 4 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 里面删除事件就好了
接下来如何从数组里面删除一个元素?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const a = [] a .splice (0 , 0 , 1 )a .splice (1 , 0 , 2 )a a .splice (1 , 1 )a .splice (0 , 0 , 3 )a a .splice (0 , 1 )a
splice 可以在任何位置增和删,相当于上面四个 api(unshift、shift、push、pop)
再来看数组的另外7个 api join&slice&sort
array.join(‘-‘) 用于将数组所有元素连接成一个字符串并返回这个字符串。
1 2 3 4 5 6 7 8 9 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) 用下标切割一个数组并返回一个新的数组对象,原始数组不会被改变。
1 2 3 4 5 6 7 8 9 10 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 属性就够了
1 2 3 array = Array.prototype.slice.call(fakeArray) 或者 array = [].slice.call(fakeArray)
ES6 看不下去这种蹩脚的转换方法,出了一个新的 api
1 array = Array .from (fakeArray)
sort((a, b) => a - b),接受的函数可传可不传 用来排序一个数组,据说大部分语言的 sort 都是用的快排,这里先简化成选择排序把(每次都选择最小的放在前面,第二次选择第二小的放在第二个,第三次选择第三小的放在第三个……,以此类推)
1 2 3 4 5 6 7 8 9 10 11 12 13 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] ] } } } }
然后在说说上面的参数,如果想从小到大排序,到底是 (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
1 2 3 4 5 6 7 Array .prototype .myForEach = function (fn ){ for (let i=0 ;i<this .length ; i++){ if (i in this ){ fn.call (undefined , this [i], i, this ) } } }
forEach 和 for 的区别主要有两个:
forEach 没法 break
forEach 用到了函数,所以每次迭代都会有一个新的函数作用域;而 for 循环只有一个作用域(著名前端面试题就是考察了这个)举例
map
1 2 3 4 5 6 7 8 9 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
1 2 3 4 5 6 7 8 9 10 11 12 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]) } } } return result }
reduce 讲了这么多,就是为了最后讲她,代码其实很简单,可能思考起来比较难 简单来说他是一个累加器,遍历的时候能把上一次的结果和这次进行操作,然后返回 举个简单例子 [1,2,3,4,5].reduce((result, item) => result + item, 0)
,输出 15
1 2 3 4 5 6 7 8 9 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 ) } } return result }
通过我们实现的源码来看,好像和之前几个 api 差不多,只是有个 result 的区别
其实正是这样,先来看看他们之前的联系
1 2 3 4 5 6 7 8 array2 = array .map ( (v ) => v+1 ) array2 = array .reduce ( (result, v )=> { result.push (v + 1 ) return result }, [ ])
1 2 3 4 5 6 7 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 LazyMan ("Hank" ) Hi! This is Hank! LazyMan ("Hank" ) .sleep (10 ).eat ("dinner" )Hi! This is Hank! 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" )Wake up after 5 Hi This is Hank! Eat supper
首先第一题简单
1 2 3 function LazyMan (name ) { console .log (`Hi! This is ${name} !` ) }
第二题也简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 ) } } } } }
第三题,稍微优化一下代码,实现一个链式调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 插队
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 ())
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 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 }