其实 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 的区别主要有两个:
- forEach 没法 break
- 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
}