現在 20-50K 的招聘,我們先看看是什麼要求? 螞蟻金服招聘要求: 蝦皮招聘: 騰訊: 明源雲: 毫無疑問,這些公司都是招聘的大前端技術棧的職位,之前文章提到過 2020 年大前端最理想的技術棧,其實真的弄得很明白那些,出去面試基本上不會有什麼問題。 小提示:如果發現小公司面試套你的技術和架構, ...
螞蟻金服招聘要求:
蝦皮招聘:
騰訊:
明源雲:
毫無疑問,這些公司都是招聘的大前端技術棧的職位,之前文章提到過 2020 年大前端最理想的技術棧,其實真的弄得很明白那些,出去面試基本上不會有什麼問題。
小提示:如果發現小公司面試套你的技術和架構,迅速結束,開出天價薪資走人。
下麵正式公佈部分面試題,以及答案:
出於對各個公司的尊重,不公佈是哪家公司的面試題,以及面試技巧。只公佈部分面試題和答案,以及分析問題的角度,學習方向,面試中考察的不僅僅技術深度,還有廣度,每個人不可能技術面面俱到,前端學習的東西太多,忘掉一部分也是正常。記住核心就是關鍵,這些都是一些基礎面試題,比較通用。
一般面試都會要做題,據我經驗看,一般都是 6 頁,三張紙。考察的大部分是前端技術棧,原生 Javascript 的內容,當然,有的外企的面試體驗更棒,技術一面規定是半個小時,國內公司可能有 5 輪,甚至 6、7 輪。
面試題我會歸納成原生 JavaScript、Node.js、React、Vue、通信協議、運維部署、CI 自動化部署、Docker、性能優化、前端架構設計、後端常見的架構等來分開寫
原生 JavaScript 篇
以下代碼跟我寫的有點不一樣,但是大致差不多,最終都是在紙上手寫實現
手寫一個深拷貝:
此處省略了一些其他類型的處理,可以在題目旁註釋、
手寫一個 reduce:
Array.isArray 的原理:
手寫一個的防抖函數:
手寫一個 Promise:
const PENDING = "pending"; const FULFILLED = "fulfilled"; const REJECTED = "rejected"; function MyPromise(fn) { const self = this; self.value = null; self.error = null; self.status = PENDING; self.onFulfilledCallbacks = []; self.onRejectedCallbacks = []; function resolve(value) { if (value instanceof MyPromise) { return value.then(resolve, reject); } if (self.status === PENDING) { setTimeout(() => { self.status = FULFILLED; self.value = value; self.onFulfilledCallbacks.forEach((callback) => callback(self.value)); }, 0) } } function reject(error) { if (self.status === PENDING) { setTimeout(function() { self.status = REJECTED; self.error = error; self.onRejectedCallbacks.forEach((callback) => callback(self.error)); }, 0) } } try { fn(resolve, reject); } catch (e) { reject(e); } } function resolvePromise(bridgepromise, x, resolve, reject) { if (bridgepromise === x) { return reject(new TypeError('Circular reference')); } let called = false; if (x instanceof MyPromise) { if (x.status === PENDING) { x.then(y => { resolvePromise(bridgepromise, y, resolve, reject); }, error => { reject(error); }); } else { x.then(resolve, reject); } } else if (x != null && ((typeof x === 'object') || (typeof x === 'function'))) { try { let then = x.then; if (typeof then === 'function') { then.call(x, y => { if (called) return; called = true; resolvePromise(bridgepromise, y, resolve, reject); }, error => { if (called) return; called = true; reject(error); }) } else { resolve(x); } } catch (e) { if (called) return; called = true; reject(e); } } else { resolve(x); } } MyPromise.prototype.then = function(onFulfilled, onRejected) { const self = this; let bridgePromise; onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value; onRejected = typeof onRejected === "function" ? onRejected : error => { throw error }; if (self.status === FULFILLED) { return bridgePromise = new MyPromise((resolve, reject) => { setTimeout(() => { try { let x = onFulfilled(self.value); resolvePromise(bridgePromise, x, resolve, reject); } catch (e) { reject(e); } }, 0); }) } if (self.status === REJECTED) { return bridgePromise = new MyPromise((resolve, reject) => { setTimeout(() => { try { let x = onRejected(self.error); resolvePromise(bridgePromise, x, resolve, reject); } catch (e) { reject(e); } }, 0); }); } if (self.status === PENDING) { return bridgePromise = new MyPromise((resolve, reject) => { self.onFulfilledCallbacks.push((value) => { try { let x = onFulfilled(value); resolvePromise(bridgePromise, x, resolve, reject); } catch (e) { reject(e); } }); self.onRejectedCallbacks.push((error) => { try { let x = onRejected(error); resolvePromise(bridgePromise, x, resolve, reject); } catch (e) { reject(e); } }); }); } } MyPromise.prototype.catch = function(onRejected) { return this.then(null, onRejected); } MyPromise.deferred = function() { let defer = {}; defer.promise = new MyPromise((resolve, reject) => { defer.resolve = resolve; defer.reject = reject; }); return defer; } try { module.exports = MyPromise } catch (e) {}
promisify 原理:
promisify = function(fn) { return function() { var args = Array.from(arguments); return new MyPromise(function(resolve, reject) { fn.apply( null, args.concat(function(err) { err ? reject(err) : resolve(arguments[1]); }) ); }); }; };
Redux 核心源碼解析: bindActionCreator 源碼解析:
export default function bindActionCreator(actions, dispatch) { let newActions = {}; for (let key in actions) { newActions[key] = () => dispatch(actions[key].apply(null, arguments)); } return newActions; }
核心:將多個 action 和 dispatch 傳入,合併成一個全新的 actions 對象
combineReducers 源碼:
export default combineReducers = reducers => (state = {}, action) => Object.keys(reducers).reduce((currentState, key) => { currentState[key] = reducers[key](state[key], action); return currentState; }, {});
核心:跟上面有點類似,遍歷生成一個全新的 state,將多個 state 合併成一個 state
createStore 源碼結合 applyMiddleware 講述如何實現處理多個中間件:
export default function createStore(reducer, enhancer) { if (typeof enhancer !== 'undefined') { return enhancer(createStore)(reducer) } let state = null const listeners = [] const subscribe = (listener) => { listeners.push(listener) } const getState = () => state const dispatch = (action) => { state = reducer(state, action) listeners.forEach((listener) => listener()) } dispatch({}) return { getState, dispatch, subscribe } }
applyMiddleware:
export default function applyMiddleware(...middlewares) { return (createStore) => (reducer) => { const store = createStore(reducer) let dispatch = store.dispatch let chain = [] const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } } }
核心:當發現傳入 createStore 的第二個或者第三個參數存在時候(這裡沒有像原生 redux 支持 SSR 代碼註水,不支持第二個參數 initState),就去返回它的調用結果
整個 Redux 這裡是最繞的,這裡不做過分的源碼講解,其實核心就是一點:
實現多個中間件原理,就是將 dispatch 當作最後一個函數傳入,利用 compose 這個工具函數,最終實現多個中間件同時起作用,當你源碼看得比較多的時候會發現,大多數的源碼是跟 redux 相似
compose 工具函數實現:
export default function compose(...funcs) { return funcs.reduce((a, b) => (...args) => a(b(...args))); }
核心:其實就是一個 reduce 函數實現,每次返回一個新的函數,再將新的參數傳入
redux 下次會專門出個文章講解,它的源碼太重要了~
原生 JavaScript 考察點比較多,這裡只列出一部分,還有像結合 TypeScript 一起問的,組合繼承,對象創建模式、設計模式等,但是那些本次不做講解
Node.js 篇幅:
簡述 Node.js 的 EventLoop:
現場出題,項目里有下麵這段代碼,輸出是什麼,穩定嗎,說明原因:
setTimeout(() => { console.log(1); }); //----若幹代碼邏輯 new Promise((resolve, reject) => { resolve(); }).then(() => { console.log(2); });
答案:先輸出2,再輸出1,但是不穩定。因為定時器的執行時間不確定,node.js 的輪詢相當於一個定時器,一直從上往下 6 個階段輪詢,此時如果中間代碼比較耗時,還沒運行到 Promise 時候,已經輪詢到第一階段,定時器的回調就會被觸發。
Node.js為什麼處理非同步IO快?
答:Node 底層採用線程池的原理管理非同步 IO,所以我們通常所的 單線程是指 Node 中 JavaScript 的執行是單線程的,但 Node 本身是多線程的。Node.js 中非同步 IO 是通過事件迴圈的方式實現的,非同步 IO 事件主要來源於網路請求和文件 IO。但是正因為如此,Node.js 處理很多計算密集型的任務,就比較吃力,當然有多進程方式可以解決這個問題。(自己給自己挖坑)
以前聽過一個很形象的回答:Java 是一百個服務員對應一百個用餐客人,Node 是一個服務員對應一百個用餐客人(因為客人不需要分分鐘服務,可能只要三分鐘----好像,東哥?)
Node.js 有 cluster、fork 兩種模式多進程,那麼這兩種情況下,主進程負責 TCP 通信,怎樣才可以讓子進程共用用戶的 Socket 對象?
答案:cluster 模式,多實例、自動共用埠鏈接、自動實現負載均衡。fork 模式實現的多進程,單實例、多進程,可以通過手動分發 socket 對象給不同子進程進行定製化處理、實現負載均衡
Node.js 多進程維護,以及通信方式:
答案:原生的 cluster 和 fork 模式都有 API 封裝好的進行通信。如果是 execfile 這樣形式調起第三方插件形式,想要與第三方插件進行通信,可以自己封裝一個類似 promisyfy 形式進行通信,維護這塊,子進程可以監聽到異常,一旦發現異常,立刻通知主進程,殺死這個異常的子進程,然後重新開啟一個子進程~
簡單談談,Node.js 搭建 TCP、restful、websocket、UDP 伺服器,遇到過哪些問題,怎麼解決的
答案:這裡涉及的問題比較多,考察全方位的通信協議知識,需要出個專題後期進行編寫
看你簡歷上寫,對 koa 源碼系統學習過,請簡述核心洋蔥圈的實現:
答案:洋蔥圈的實現,有點類似 Promise 中的 then 實現,每次通過 use 方法定義中間件函數時候,就會把這個函數存入一個隊列中,全局維護一個 ctx 對象,每次調用 next(),就會調用隊列的下一個任務函數。偽代碼實現~:
use (fn) { // this.fn = fn 改成: this.middlewares.push(fn) // 每次use,把當前回調函數存進數組 } compose(middlewares, ctx){ // 簡化版的compose,接收中間件數組、ctx對象作為參數 function dispatch(index){ // 利用遞歸函數將各中間件串聯起來依次調用 if(index === middlewares.length) return // 最後一次next不能執行,不然會報錯 let middleware = middlewares[index] // 取當前應該被調用的函數 middleware(ctx, () => dispatch(index + 1)) // 調用並傳入ctx和下一個將被調用的函數,用戶next()時執行該函數 } dispatch(0) }
所以這裡說,源碼看多了會發現,其實大都差不多,都是你抄我的,我抄你的,輪子上搭積木。
你對 TCP 系統學習過,請你簡述下 SYN flood 攻擊:
答案:攻擊方偽造源地址發送 SYN 報文,服務端此時回覆 syn+ack,但是真正的 IP 地址收到這個包之後,有可能直接回覆了 RST 包,但是如果不回覆 RST 包,那就更嚴重了,可能服務端會在幾十秒後才關閉這個 socket 鏈接(時間根據每個系統不一樣)
抓包可見~:
TCP 可以快速握手嗎?
答案:可以 -- 內容來自 張師傅的小冊
TCP 鏈接和 UDP 的區別,什麼時候選擇使用 UDP 鏈接?
總結就是:TCP 面向鏈接,UDP 面向消息,TCP 的 ACK 作用是確認已經收到上一個包,UDP 只管發送,一些無人機的操作,就用 UDP 鏈接,每個無人機就是一個伺服器,跟地面通訊。
通信協議還是要系統學習,通信這裡也問了大概半個小時,包括密鑰交換等。
看你簡歷上有寫自己實現了一個 mini-react,請簡述實現原理,以及 diff 演算法實現
答案:利用了 babel,將虛擬 dom 轉換成了我想要的對象格式,然後實現了非同步 setState、component diff 、 element diff 、props 更新等。類似 PReact 的將真實 dom 和虛擬 dom 對比的方式進行 diff,這裡結合代碼講了大概半個小時~ 大家可以看源碼,這個對於學習 React 是非常好的資料,當時我花了半個多月學習。
看你對 Vue 的源碼有系統學習過,請簡述下 Vue2.x 版本的數據綁定:
答案:Vue 裡面的 {{}} 寫法, 會用正則匹配後,拿到數據跟 data 里的做對比-解析指令,觀察數據變化是利用 defineProperty 來實現,因為監聽不到數組的變化,所以尤大大隻重寫了 6 個數組 API。源碼解析,後面就是拼細節,主要講一些核心點的實現。
為什麼 Vue 的 nextTick 不穩定?
答案:Vue 的 nextTick 原理是:
優雅降級: 首選 promise.then 然後是 setImmediate 然後是一個瀏覽器目前支持不好的 API 最後是 setTimeout
dom 真正更新渲染好的時間,不能真正確定,不論是框架還是原生,都存在這個問題。所以用 nextTick 並不能保證拿到最新的 dom。
談談你對微前端的看法,以及實踐:
答案:將 Vue 和 React 一起開發,其實一點都不難,只要自己能造出 Redux 這樣的輪子,熟悉兩個框架原理,就能一起開發,難的是將這些在一個合適的場景中使用。之前看到網上有微前端的實踐,但是並不是那麼完美,當然,類似 Electron 這樣的應用,混合開發很正常,微前端並不是只單單多個框架混合開發,更多是多個框架引入後解決了什麼問題、帶來的問題怎麼解決?畢竟 5G 還沒完全普及,數據傳輸還是不那麼快。過大的包容易帶來客戶端的過長白屏時間(自己給自己挖坑)
你有提到白屏時間,有什麼辦法可以減少嗎?都是什麼原理?
答案:GZIP,SSR 同構、PWA 應用、預渲染、localStorage 緩存 js 文件等。
下麵就是細分拆解答案,無限的連帶問題,這裡非常耗時,這些內容大都網上能搜到,我這裡就不詳細說
其中有問到 PWA 的原理,我的回答是: Service Worker 有一套自己的聲明周期,當安裝並且處於激活狀態時候,網站在 https 或者 localhost 的協議時候,可以攔截過濾發出的請求,會先把請求克隆一份(請求是流,消費就沒有了),然後判斷請求的資源是否在 Service Worker 緩存中,如果存在那麼可以直接從 Service Worker 緩存中取出,如果不存在,那麼就真正的發出這個請求。
看你的技術棧對 Electron 比較熟悉,有使用過 React-native,請你談談使用的感受?
答案:React-native 的坑還是比較多,但是目前也算擁有成熟的生態了,開發簡單的 APP 可以使用它。但是複雜的應用還是原生比較好,Electron 目前非常受歡迎,它基本上可以完成桌面應用的大部分需求,重型應用開發也是完全沒問題的,可以配合大量 C# C++ 插件等。
Node.js 的消息隊列應用場景是什麼?原理是什麼?
答案:我們公司之前用的 kafka,消息隊列的核心概念,非同步,提供者,消費者。例如 IM 應用,每天都會有高峰期,但是我們不可能為了高峰期配置那麼多伺服器,那樣就是浪費,所以使用消息隊列,在多長時間內流量達到多少,就控制消費頻率,例如客戶端是流的提供者,有一個中間件消費隊列,我們的伺服器是消費者,每次消費一個任務就回覆一個 ACK 給消費隊列,消費頻率由我們控制,這樣任務不會丟失,伺服器也不會掛。 還有一個非同步問題,一個用戶下單購買一件商品,可能要更新庫存,已購數量,支付,下單等任務。不可能同步進行,這時候需要非同步並行,事務方式處理。這樣既不耽誤時間,也能確保所有的任務成功才算成功,不然沒有支付成功,但是已購數量增長了就有問題。
此處省略、、、一萬字。
用戶就是要上傳 10 個 G 的文件,伺服器存儲允許的情況下,你會怎麼處理保證整體架構順暢,不影響其他用戶?
答案:我會準備兩個伺服器上傳介面,前端或者原生客戶端上傳文件可以拿到文件大小,根據文件大小,分發不同的對應伺服器介面處理上傳,大文件可以進行斷點續傳,原理是 md5 生成唯一的 hash 值,將分片的 hash 數組先上傳到後端,然後將文件分片上傳,對比 hash 值,相同的則丟棄。不一致的話,根據數組內容進行 buffer 拼接生成文件。
關於伺服器性能,大文件上傳的伺服器允許被阻塞,小文件的伺服器不會被阻塞。
談談你對前端、客戶端架構的認識?
答案:前端的架構,首先明確項目的相容性,面向瀏覽器編程,是否做成 PC、移動端的響應式佈局。根據項目規模、後期可能迭代的需求制定技術方案,如果比較重型的應用應該選用原生開發,儘量少使用第三方庫。
客戶端架構:是否跨平臺,明確相容系統,例如是否相容 XP ,如果相容 XP 就選擇 nw.js,再然後根據項目複雜度招聘相應技術梯度人員,安排系統學習相關內容,招聘人員或者購買定製開發相關原生插件內容。
雖然說只是談談,但是感覺面試的職位越高級、輪數越往後,越考驗你的架構能力,前面考察基礎,後面考察你的技術廣度以及邏輯思維,能否在複雜的應用中保持清醒頭腦,定位性能這類型的細節能力。很多人基礎面試面得很好,但是拿不到 offer,原因就是沒有這種架構能力,只能自己寫代碼,不能帶領大家學習、寫代碼。這也是我在面試時偶然聽到某個大公司 HR 之間的對話,原話是:他面試還可以,看起來是很老實(某個之前的面試者),但是他對之前項目整體流程並不是那麼清楚,連自己做的項目,前後端流程都不清楚,感覺不合適。
介紹一下 Redis,為什麼快,怎麼做持久化存儲?
答案:Redis 將數據存儲在記憶體中,key-value 形式存儲,所以獲取也快。支持的 key 格式相對於 memorycache 更多,而且支持 RDB 快照形式、AOF。
RDB 持久化是指在指定的時間間隔內將記憶體中的數據集快照寫入磁碟,實際操作過程是 fork 一個子進程,先將數據集寫入臨時文件,寫入成功後,再替換之前的文件,用二進位壓縮存儲。RDB 是 Redis 預設的持久化方式,會在對應的目錄下生產一個 dump.rdb 文件,重啟會通過載入 dump.rdb 文件恢複數據。
優點:
1)只有一個文件 dump.rdb,方便持久化;
2)容災性好,一個文件可以保存到安全的磁碟;
3)性能最大化,fork 子進程來完成寫操作,讓主進程繼續處理命令,所以是 IO 最大化(使用單獨子進程來進行持久化,主進程不會進行任何 IO 操作,保證了 redis 的高性能) ;
4)如果數據集偏大,RDB 的啟動效率會比 AOF 更高。
缺點:
1)數據安全性低。(RDB 是間隔一段時間進行持久化,如果持久化之間 redis 發生故障,會發生數據丟失。所以這種方式更適合數據要求不是特別嚴格的時候)
2)由於 RDB 是通過 fork 子進程來協助完成數據持久化工作的,因此,如果當數據集較大時,可能會導致整個伺服器停止服務幾百毫秒,甚至是 1 秒鐘。
AOF 持久化是以日誌的形式記錄伺服器所處理的每一個寫、刪除操作,查詢操作不會記錄,以文本的方式記錄,文件中可以看到詳細的操作記錄。她的出現是為了彌補 RDB 的不足(數據的不一致性),所以它採用日誌的形式來記錄每個寫操作,並追加到文件中。Redis 重啟的會根據日誌文件的內容將寫指令從前到後執行一次以完成數據的恢復工作。
優點:
1)數據安全性更高,AOF 持久化可以配置 appendfsync 屬性,其中 always,每進行一次命令操作就記錄到 AOF 文件中一次。
2)通過 append 模式寫文件,即使中途伺服器宕機,可以通過 redis-check-aof 工具解決數據一致性問題。
3)AOF 機制的 rewrite 模式。(AOF 文件沒被 rewrite 之前(文件過大時會對命令進行合併重寫),可以刪除其中的某些命令(比如誤操作的 flushall))
缺點
1)AOF 文件比 RDB 文件大,且恢復速度慢;數據集大的時候,比 RDB 啟動效率低。
2)根據同步策略的不同,AOF 在運行效率上往往會慢於 RDB。
介紹下緩存擊穿和穿透:
緩存穿透:是指查詢一個資料庫一定不存在的數據。正常的使用緩存流程大致是,數據查詢先進行緩存查詢,如果 key 不存在或者 key 已經過期,再對資料庫進行查詢,並把查詢到的對象,放進緩存。如果資料庫查詢對象為空,則不放進緩存。
緩存擊穿:是指一個 key 非常熱點,在不停的扛著大併發,大併發集中對這一個點進行訪問,當這個 key 在失效的瞬間,持續的大併發就穿破緩存,直接請求資料庫,就像在一個屏障上鑿開了一個洞。
介紹下你會用的自動化構建的方式:
答案:
1)Jenkins 自動化構建
2)自己搭建 Node.js 伺服器,實現 Jenkins
3)Docker 配合 Travis CI 實現自動化構建
Jenkins 自動化構建:
配置,自動同步某個分支代碼,打包構建。
自己搭建 Node.js 伺服器,實現 Jenkins:
自己搭建 Node.js 的伺服器,在 GitLab 上指定 webhook 地址,分支代碼更新觸發事件,伺服器接受到 post 請求,裡面附帶分支的信息,執行自己的 shell 腳本命令,指定文件夾,構建打包。
伺服器上使用 Docker-compose 指定鏡像,每次代碼推送到 gitHub,通過自己編寫的 yml 和 dockerfile 文件構建打包,伺服器自動拉取最新鏡像並且發佈到正式環境。
代碼實現:
.travis.yml language: node_js node_js: - '12' services: - docker before_install: - npm install - npm install -g parcel-bundler script: - parcel build ./index.js - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - docker build -t jinjietan/mini-react:latest . - docker push jinjietan/mini-react:latest dockerfile: FROM nginx COPY ./index.html /usr/share/nginx/html/ COPY ./dist /usr/share/nginx/html/dist EXPOSE 80
附帶的一些問題:
Linux 常見操作、雲端部署等。
這是我的知乎專欄的一篇綜合性文章,裡面彙集了web前端技術乾貨、前端面試題系列、技術動向、職業生涯、行業熱點、職場趣事等一切有關於程式員的高質量文章和學習資料分享