其實 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
}