作者:Uni (感謝小夥伴投稿)
原文:https://fwf-studio.feishu.cn/docs/doccnqyJfOyD3Q5JPG2K0PfAPnf#
個人背景介紹
嗨咯大家好,我是 Uni。本人就讀於某雙非一本大學計算機系,大一的時候在疲於提升績點後,發現自己根本不知道計算機有哪些領域,能夠幹啥。於是在互聯網上廣泛搜索計算機有哪些領域、需要學什麼、能幹什麼後,確定了自己喜歡的領域:前端。在我看來前端就是美學與邏輯的完美結合,也是那個時候堅定了自己的座右銘:無論是技藝、構圖、還是大膽的想法,達到極致,即為藝術。於是在大一下,我開始了我的前端自學之路。
後來大二的時候跟志同道合的朋友們一起組建了一個技術組織,一起寫項目、學技術,也是一段快樂時光了。
在今年的3月份(2021年),考慮到家裡和學業的緣故,在實習和字節跳動訓練營之間選擇了參加字節跳動的前端訓練營,並取得了不錯的名次。原本是要準備秋招的,但是暑假因為算法比賽和其他緣故又擱置了。
因為一直很喜歡網易雲音樂這個產品,從大三的時候就一直 follow 網易雲音樂大前端團隊在掘金上的文章,並一直在關注着網易雲音樂的崗位。然後很幸運地發現網易雲音樂現在正在招實習生於是便投了。網易的效率很高,兩三天後就約面試了,所以最近有且僅面了網易。
因為最近半年基本因為一些原因一直在打算法比賽和學機器學習相關的東西,一直沒怎麼碰過 Web,所以在面試之初還是有些緊張的。
技術一面面試官人很nice,一開始怕我緊張就一直讓我介紹自己的項目。一面主要是在深挖項目。
介紹下在字節跳動訓練營的這個項目回答:這個項目是一個在線 markdown 編輯器,用的是 React 及其相關生態做的前端,Koa2 做的服務端,採用的MongoDB數據庫。有郵箱驗證、JWT鑒權、文件上傳並返回鏈接、將 markdown 語法編寫的文本轉換成 HTML、採用 websocket 做的在線錯誤日誌等功能。
為什麼要做這個基於 websocket 的在線錯誤日誌?主要是怎麼實現的?回答:做這個功能的原因主要是當時為了練技術,並沒有從整個產品的角度去考慮這一塊功能,僅僅是為了實現而實現。在實現上,首先我後端的所有響應類型都是基於SuccessModel和ErrorModel這兩個類產生的,這樣能夠保證我的響應格式的統一(都是msg、data、code)
然後通過 Node 中fs模塊的appendFile方法將錯誤信息傳入errorLog.txt這個文件,如果沒有這個文件是會自動生成的。
然後在管理員打開錯誤日誌的前端頁面的時候會建立websocket連接,並將errorLog.txt文件中的記錄當作歷史日誌傳給前端並倒序渲染出來。這個時候同時調用fs.watch方法對errorLog.txt文件的變化進行監聽,如果有錯誤日誌寫入文件中,那麼文件就變化了,就會通過 websocket 將新增的錯誤日誌記錄主動廣播給前端,以此達到管理員在日誌界面時可以看到實時的錯誤信息的效果。
PS:做這個功能的目的是為做而做,並沒有考慮那麼多,也沒有過這種場景的經驗,所以做的很不規範。只是為了嘗試、鍛煉一下。
面試官:我看到你簡歷上有一個在線聊天室的項目,用到了 socket.io 來做實時通訊這一塊,而你訓練營的項目用的是 ws 這個 npm 庫,能說說為什麼用 socket.io 嗎以及 socket.io 和 ws 之間的對比。
回答:用 socket.io 主要原因還是為了嘗試新的東西,其實這兩個我都沒有鑽的很深,只是為了需求去實現。
我所了解到的是,相比於 ws,socket.io 在客戶端有良好的支持,但是 ws 沒有,在客戶端寫的時候還需要自己去封裝。其次就是 socket.io 是有回退方案的,在不支持 websocket 的時候會回退到 HTTP 長輪訓的方案。(這一塊答的不怎麼好,因為確實對這兩個庫使用的不多,理解的不深)
能說說你項目中圖片上傳那一塊是怎麼實現的嗎?
回答:在項目的一開始,我是採用 base64 的方式實現的圖片上傳,但是這樣的話每次請求都會傳一個超長的字符串,這樣會占據更多的空間資源。所以後來我換成了文件流的形式上傳。前端通過事件對象和 FormData 的配合,將數據傳給後端。

然後後端我引用了formidable-upload-koa和fs-extra這兩個庫,將傳過來的文件格式進行解析並移動到一個暴露在外的可訪問目錄下,最後再將文件路徑存於數據庫中並返迴路徑給前端,前端每次獲取圖片只需要請求暴露在外的路徑即可,就相當於是做了一個簡陋的圖床。
(反思:面試官問完我這個問題後,我雖然說出了自己實現的思路以及為什麼用這個方案的原因,但是卻沒有實打實地研究過這兩個方案到底適合什麼場景,也沒有仔細思考過到底是不是很項目,只是為了用技術而用。通過前面幾個問題發現了自己思維上的大短板,就是很少思考項目本身需要什麼技術,用什麼技術合適。)
如果有海量請求來了,你在項目中是如何處理這些高並發請求的呢?回答:因為我沒有實際遇到過這種場景所以我也沒有具體了解過相關的解決方案。但是 I/O 密集型是 Node的強項,我後端所有的 I/O 處理都是採用異步的方式。然後前端也會對一些操作做防抖節流,來防止一些無效或者重複的請求。
你剛剛說到了防抖節流,能講講他們之間的區別嗎?回答:防抖在單位時間內觸發的事件會被重置,防止誤觸多次事件。節流就是單位時間內只觸發一次。(回答完我摸了摸鍵盤準備等着面試官讓我手寫防抖節流,但是他沒有繼續往下問了)
你項目都是 React 是吧?(是的,我 hook 寫的比較多)那你介紹一下你常用的 hook 吧說了幾個常用的 hook,然後重點講了一下useEffect和useLayoutEffect的區別,通過他們渲染時機的區別講了講項目中遇到過的頁面閃爍的問題並怎麼解決的。
那你來手寫實現一下這道題吧classEvent{//觸發事件trigger(eventName){}//註冊事件on(eventName,callback){}//銷毀事件off(eventName){}}//caseconstevent=newEvent()event.on("eventA",(a)=>{console.log(a)})event.on("eventA",(a,b)=>{console.log(a)console.log(b)})event.trigger("eventA",1,2)//print:112我的實現:
classEvent{constructor(){this.map=newMap()}//註冊事件on(eventName,callback){!this.map.has(eventName)&&this.map.set(eventName,[])constarr=this.map.get(eventName)arr.push(callback)this.map.set(eventName,arr)}//觸發事件trigger(eventName,...args){constarr=this.map.get(eventName)arr.forEach(item=>{item(...args)})}//銷毀事件off(eventName){this.map.delete(eventName)}}追問:如果這個off方法加一個callback參數,我想要每次註銷的事件是這一組同名事件中的具體的某一個呢?
回答:如果這個callback是外部作用域有引用的,然後傳入參數的話,那直接forEach判斷一下是否相等然後直接對數組使用splice方法刪除即可,因為都是同一個引用嘛。如果是這種場景的話我這裡就可能用WeakMap會比Map好些,這樣可以防止內存泄漏的情況發生
技術二面面試官人也很nice,我運氣真的太好了,遇到的都是彬彬有禮、很是溫和的面試官。首先還是上來跟面試官介紹了一下項目,這裡就簡單跳過。
你對跨域的方案有了解嗎?你的項目里是怎麼實現跨域的?我了解到的跨域方案有jsonp、CORS、postMessgae以及Websocket。在我的項目中用的是CORS跨域的方案。
你剛剛說到CORS跨域,哪請問options是在什麼情況下觸發的呢?回答:不會(這個真沒了解到) 下來後立馬百度了解了相關知識:瀏覽器會對於非簡單請求會觸發一次預檢的請求,對應的 HTTP Request Method 為 OPTIONS。這個請求對服務器是安全的,也就是說不會對服務器的資源做任何改變,僅僅用於確認 header 響應。具體的歡迎參考這篇文章進行了解: https://zhuanlan.zhihu.com/p/70032617
讓我手寫 JS 的繼承方式先問:你平時都看什麼技術書
再問:那你能講講這些書裡面讓你印象深刻的知識點嗎
繼續問:那你說說有什麼繼承方式吧(其實我很想讓面試官問我JS整個執行機制和詞法作用域具體機制來着,整個當時深鑽了很長時間)
回答(讓我手寫 JS 的繼承方式,我寫了三種):
//1.藉助原型鏈functionParent(){this.name="Uni"}functionChild(){}Child.prototype=newParent()Child.prototype.constructor=Child//2.藉助call方法functionParent(){this.name="Uni"}functionChild(){Parent.call(this)this.age=21}constchild=newChild()//3.寄生組合式繼承functionclone(parent,child){child.prototype=Object.create(parent.prototype)child.prototype.constructor=child}functionParent(){this.name="Uni"}functionChild(){Parent.call(this)this.age=21}clone(Parent,Child)面試的時候其實我是邊寫邊跟面試官說每種寫法的優缺點和為什麼用這種寫法,所以自然而然地按照這個順序寫下來了,但是由於有些緊張加上邊寫邊說,所以有些磕磕絆絆,但是總的還是回答完了這個問題。
如果有一個模態框,想要點擊模態框以外的區域讓模態框消失,應該怎麼做?回答:我從事件冒泡和事件捕獲兩種方式進行了回答。冒泡的方式很好地答了出來,但是面試官一直在不斷追問我一些情形,然後我腦子卡殼了捕獲就沒有答的很好。因為是面經具體答案就不闡述了,這個問題手動寫寫試一下就知道答案了。
CSS優先級順序能說說嗎?回答:當時因為還在糾結前面捕獲的事情,腦子一團混亂,就迷迷糊糊答得很差。面完後重新看了下這塊的內容,具體可以參考這兩篇文章:
1、https://zhuanlan.zhihu.com/p/416047752、https://zhuanlan.zhihu.com/p/23047507
有讀過什麼框架和庫的源碼嗎(我其實研究過一段時間的 React 和 JQuery,但是因為最近半年一直在打算法和研究機器學習方面的內容,前端很多東西都忘了,就求穩說了一個 JQuery),請講講JQuery 源碼方面的內容。
追問:JQuery 是如何做到鏈式調用的
就用一個輔助棧就好了,感興趣的朋友可以去刷一下
反思與總結其實說實話,面經中寫面試題很難說起到一個參考或者幫助作用,因為面試題是不固定的、面試官是不確定的、面試難度和應聘者實力也是不一定匹配的。這就像考試一樣,有些人拿一百分是因為卷子滿分就一百分。
但是面試和考試不一樣的地方在於,面試是你與面試官交流的媒介,是要體現個人綜合實力的地方,也是一個學習的渠道。所以我認為面經最重要的地方其實在於面試者下來的反思和思考。
我的反思通過這次面試,其實我最大的感觸是對業務上思考太少,我很少會去從產品功能的角度去思考我的技術。這會導致我的技術方案或者做法缺少業務價值。比如在做在線 markdown 編輯器的時候我的關注點只在於我怎麼做出來這個語法轉譯的功能,但卻沒有思考如果從用戶角度想要定製我的一些 markdown 語法轉譯後的樣式我該怎麼去做,所以我的整個功能可擴展性就非常地低。因為缺少業務上的思考,所以我開發的時候會更難想到一些更隱性的場景,比如性能優化、功能的可擴展性和完備性。這是我需要再多刻意練習的。
關於面試的建議首先,我覺得應該抱着學習的心態多面試,不論是找實習的同學還是找校招的同學。面試就是一個從面試官那取經、發現不足的地方。所以一定一定不要緊張,要放開手腳來。在面試的過程中可以多跟面試官交流,可以根據某道題聊聊你的思考,也可以在反問環節向面試官請教自身的不足,也可以帶着面試官一起剖析反思自己。我面試的時候就是抱着這樣的心態去跟面試官交流,最後也都跟面試官們聊開了,也得到了許多寶貴的建議。真就是面到就是賺到!最後,不得不說整個流程下來,我的體驗是非常好的,HR推進流程的效率非常高,面試官也都很好溝通願意引導我給我一些建議。

