來聊一道前端面試題吧

愛前端也愛戀愛發表於2020-10-27

前言

金三銀四,技術論壇上諸如:阿里、頭條、騰訊….面經層出不窮,朋友圈很多小夥伴都在找工作也遇到了各種各樣的麻煩。本文希望那些在準備面試的過程中蕉綠的童鞋別僵化了自己的思維,以自己曾經遇見到一道面試題為引,用自己對待問題的想法行文,天馬行空,從僵硬的知識點中跳脫出來一起思考,內容簡單易懂。評論區有很多的同學留下了許多很棒的思路,大家不要錯過喲,歡迎大家一起繼續交流學習。

“我自己是一名從事了8年web前端開發的老程式設計師(我的微信:webxxq),今年年初我花了一個月整理了一份最適合2020年自學的web前端全套培訓教程((視訊+筆記+素材+原始碼+專案實戰),從最基礎的HTML+CSS+JS到移動端HTML5以及各種框架和新技術都有整理,打包給每一位前端小夥伴(總共約85G),這裡是前端學習者聚集地,歡迎初學和進階中的小夥伴(所有前端教程關注我的微信公眾號:web前端學習圈,關注後回覆“2020”即可領取)。

const fucArr = [
	next => {
		setTimeout(() => {
			console.log(1);
			next()
		}, 300)
	},
	next => {
		setTimeout(() => {
			console.log(2);
			next()
		}, 200)
	},
	next => {
		setTimeout(() => {
			console.log(3);
			next()
		}, 100)
	}
]

var run = arr=>{

  

}
// 實現一個run方法,使得run(fucArr)能順序輸出1、2、3.

複製程式碼

題目簡析

我們觀察 fucArr 每一個子項都具有如下結構:

  1. 接收一個方法 next
  2. 有一個計時器,計時器回撥方法體內對應著相應的輸出
  3. 輸出結束呼叫 next 方法。

他們的差異就是:計時器時間逐個減少。

直接迴圈呼叫 3 個方法肯定是不可取的。為了能按序輸出,函式的執行過程應該是上一個函式 console 之後, 再執行下一個函式,而接收的這個 next引數就是執行下一個方法的關鍵。因為是頭到尾依次呼叫,我們就把fucArr 稱之為一個佇列。

思路一、

我們假象自己是個編譯器,然後把執行的過程進行單步拆解。

  1. fucArr 是做先執行等待佇列第一個,等待中的函式佇列為原函式佇列的slice(1);
  2. 等待next執行後,然後又執行等待函式佇列的第一個函式,等待中的函式佇列為原函式佇列的slice(1);

聽著是不是很像一個遞迴的過程,沒錯,那我們先用遞迴來實現一下

var run = arr => {
	// 遞迴語句千萬條,找到出口第一條,那我們們判斷遞迴出口的條件就是等待佇列為空
	if (arr.length === 0) return;
	// 好的,一句話執行過程寫完了
	arr[0](() => run(arr.slice(1)));
}
run(fucArr)

// 1 2 3;
複製程式碼

思路二、

現在我們從遞迴的思路中跳脫出來,換種思路繼續思考.....

上一個函式執行到某個時機觸發了下一個函式的執行。

也就是說上一個函式 trigger,下一個函式才開始執行。

根據描述 trigger 實際上做的就是觸發等待佇列的第一個函式的執行,因此我們可以如下定義。

var run = arr => {
	const trigger = () => {
		if (arr.length === 0) return;
		arr.shift()();
	}
}
複製程式碼

那麼 trigger 何時進行呼叫呢?很顯然, 上一個函式式通過next 去觸發下一個函式呼叫,因此 trigger 應該就是函式接收的next,我們為了方便引數繫結需要重構一下我們們的等待佇列函式。當然不要忘了,首次執行要手動trigger一下喔。

var run = arr => {
	const trigger = () => {
		if (arr.length === 0) return;
		arr.shift()();
	}
	arr = arr.map(val => {
		return () => val(trigger);
	})
	trigger();
}
複製程式碼

其實做引數繫結還有一種更優雅一點的方式,bind,所以大家注意咯,bind不單單能繫結this喔。

我們可以稍微改動一下:

var run = arr => {
	const trigger = () => {
		if (arr.length === 0) return;
		arr.shift()();
	}
	arr = arr.map(val => {
		return val.bind(null, trigger);
	})
	trigger();
}
複製程式碼

都9102年了,既然是前端面試那肯定少不了Promise 的對吧,那我們可不可以摻入一些Promise的元素在裡面呢?答案是必然的。

根據Promise的特性,當本身狀態改變,去觸發then裡的方法(這裡不要深究這句話,意思瞭解就好)。是resolve 作為本身狀態改動的方法。那狀態改變是去做什麼事呢?好的,沒錯trigger。那何時狀態改變呢?上一個函式next呼叫的時候。

var run = arr => {
	const trigger = () => {
		if (arr.length === 0) return;
		arr.shift()();
	}
	arr = arr.map(val => {
		return () => new Promise(resolve => {
			val(resolve)
		}).then(trigger);
	})
	trigger();
}
複製程式碼

redux的思路、

現在繼續清空上面的思路,不要被干擾。

首先給 applymiddleware(以下簡稱amw)一個簡單的定義,amw是接收若干個函式作為引數,最終會返回一個函式,這個函式呼叫,會按照順序,依次執行前面作為引數傳入的函式。為了不把問題複雜化,請接收我的誤導引導,不要懷疑。

以下是作為引數傳入的函式要求的結構以下稱a結構:

store=>next=>action=>{
	// dosomething...
	next()
}
複製程式碼

a結構在第一次呼叫時,會返回一個方法,第二次呼叫時返回第二個方法,我們先來看原始碼的一個操作過程。

const chain = middlewares.map(middleware => middleware(middlewareAPI))
複製程式碼

首先是一層迴圈呼叫,使得函式體變為b結構:

next=>action=>{
	// dosomething...
	next()
}
複製程式碼

這樣做是為了以閉包的形式在 dosomething 中能夠使用到 middlewareApi

根據b結構我們可以稍稍改變下原題 :

const fucArr = [
	next => action => {
		setTimeout(() => {
			console.log(action++);
			next(action)
		}, 300)
	},
	next => action => {
		setTimeout(() => {
			console.log(action++);
			next(action)
		}, 200)
	},
	next => action => {
		setTimeout(() => {
			console.log(action++);
			next(action)
		}, 100)
	}
]

var run = arr=>{

  

}
// 實現一個run方法,run方法接收fucArr為引數;返回一個函式,這個函式接收一個引數1,最終,依次輸出1、2、3
// run(fucArr)(1) => 1 2 3
複製程式碼

變題相對於多了一個引數傳遞的過程,實際上我們需要順序執行的其實是結構c:

action=>{
    // dosomething...
	next()
}
複製程式碼

這些關鍵還是要如何構建每個函式接收的引數next

我們做如下假設,當fucArr只有一個函式時 返回的就應該是:

fucArr[0](()=>{}) // 為了避免報錯,next應為一個空函式
// 即:
action => {
    setTimeout(() => {
        console.log(action++);
        //(()=>{}) 這玩意兒就是接收的next
        (()=>{})(action)
    }, 300)
}
複製程式碼

fucArr有兩個函式時返回:

fucArr[0](fucArr[1](()=>{}))
// 即:
action => {
    setTimeout(() => {
        console.log(action++);        
		fucArr[1](()=>{})(action)
    }, 300)
}
複製程式碼

當有三個函式的時返回:

fucArr[0](fucArr[1](fucArr[2](()=>{}))
複製程式碼

仔細觀察返回函式的結構發現,所有的函式都是接受上一個函式呼叫後的返回值(以下稱模式1),最後一個函式接收的是一個空函式。我們嘗試構建模式1:

// 首先初始想法模型是這樣的
// 但是由於我們們是程式執行,不能像上面我們們描述問題的時候,繼續往next裡塞函式。
// 而在遍歷到 next 的下一個函式的時當前是無法明確next應該是什麼,因此我們需要將模式改變一下。
pre(next());
// 當遍歷到next下一個節點時,把當前函式作為arg傳入進來
arg=>pre(next(arg))
複製程式碼

pre + next + 遍歷,這三個關鍵詞沒錯,就是reduce。因此:

var reduceResult = fucArr.reduce((pre,next)=> (...arg)=>pre(next(...arg)));
// 我們發現這個返回的還是一個 arg=>pre(next(arg)) 這樣模式的函式,接收的引數任然是一個函式。
// 於是乎真的需要返回的函式其實是 
return reduceResult(()=>{});
複製程式碼

所以最終形態是

var run = arr=>{
	var reduceResult = arr.reduce((pre,next)=> (...arg)=>pre(next(...arg)));
	return reduceResult(()=>{});
}
run(fucArr)(1);
// 1 2 3
複製程式碼

總結

其實還可以聊下expresskoa中介軟體compose的思路,但是沒有必要(汪汪大笑.gif)。本文主旨也不是灌輸這個題目的解法,只是希望大家將來在面試和工作中遇到問題嘗試著用自己構建的知識體系去解決積極面對,最後祝小夥伴們找工作順利。

相關文章