軟體工程師職涯升級計畫啟動!立即預約職涯諮詢、履歷健檢或模擬面試👈,為您的加薪做好準備!
前端工程師的求職之路,往往伴隨著一輪又一輪的技術面試。不同公司各有側重,從 JavaScript 核心、框架應用到 Git 操作,都可能是考察的重點。本文將帶你深入了解三家知名公司——Pelith、Xrex 和 Garena 的前端面試題目,並提供詳盡的解析與答案,希望能助你在面試中過關斬將,拿到心儀的 Offer!
第一站:Pelith - 紮實的 JavaScript 基礎功
Pelith 的面試似乎特別看重求職者對 JavaScript 核心概念的理解,尤其是異步處理和基礎型別判斷。
面試題目 1:Promise- 考察重點: 理解 JavaScript 異步編程的核心機制,從 Callback Hell 到 Promise,再到
async/await
的演進。 - 解析與答案:
- 異步的必要性: JavaScript 是單線程的,為了避免耗時操作(如網路請求、文件讀寫)阻塞主線程,導致頁面卡頓,異步編程應運而生。
- Promise: Promise 是一個代表了「未來」才會結束的事件(通常是一個異步操作)最終結果的對象。它有三種狀態:
- pending:初始狀態,既不是成功,也不是失敗。
- fulfilled:意味著操作成功完成。
- rejected:意味著操作失敗。
- 核心方法:
- .then(onFulfilled, onRejected):用於處理 fulfilled 狀態(onFulfilled)和 rejected 狀態(onRejected)。它返回一個新的 Promise,形成鏈式調用。
- .catch(onRejected):.then(null, onRejected) 的語法糖,專門用於處理錯誤。
- .finally(onFinally):無論 Promise 最終是 fulfilled 還是 rejected,都會執行的回調。
async/await
: ES2017 引入的語法糖,建立在 Promise 之上,讓異步代碼看起來更像同步代碼,提高可讀性。- async 關鍵字用於聲明一個函數是異步函數,該函數會隱式返回一個 Promise。
- await 關鍵字只能在 async 函數內部使用,它會暫停 async 函數的執行,等待 await 後面的 Promise 完成,然後恢復執行並返回 Promise 的結果。如果 Promise 被拒絕,await 會拋出錯誤,需要使用 try...catch 語句來捕獲。
面試題目 2:typeof
- 考察重點: 掌握
typeof
運算符的行為,了解其返回值以及一些特殊情況。 - 解析與答案:
typeof
是一個一元運算符,用於返回一個表示未經計算的操作數的類型的字符串。- 常見返回值:
- 'undefined':未定義的變數。
- 'boolean':布爾值 (true / false)。
- 'string':字符串。
- 'number':數字 (包括 NaN 和 Infinity)。
- 'bigint':大整數 (ES2020 新增)。
- 'symbol':符號 (ES2015 新增)。
- 'function':函數。
- 'object':物件、陣列 (Array)、null。
- 特別注意:
- typeof null 返回 'object':這是 JavaScript 長久以來的一個 bug,為了兼容性一直保留。判斷 null 需要使用 === null。
- typeof [] (陣列) 返回 'object':判斷陣列需要使用 Array.isArray()。
- typeof NaN 返回 'number':NaN (Not a Number) 本身是數字類型,表示一個無效的數字。判斷 NaN 需要使用 Number.isNaN() 或 isNaN() (後者會進行類型轉換,需注意)。
第二站:Xrex - React 深度與 JavaScript 廣度並重
Xrex 的面試題目涵蓋範圍更廣,從 React 的進階 Hook、異步實踐,到 Git 版本控制和 JavaScript 的核心底層機制(事件循環、閉包),考察求職者的綜合能力。
面試題目 1: useEffect
v.s useLayoutEffect
- 考察重點: 理解 React 中處理副作用的兩個 Hook 的區別、執行時機和適用場景。
- 解析與答案:
- 共同點: 兩者都用於在函數組件中執行副作用操作(如數據獲取、訂閱、手動修改 DOM)。它們都接收一個函數和一個可選的依賴數組。
- 核心區別(執行時機):
- useEffect:在瀏覽器完成繪製 (paint) 之後異步執行。它不會阻塞瀏覽器的繪製過程。這是處理大多數副作用的首選 Hook。
- useLayoutEffect:在所有 DOM 變更之後,瀏覽器進行繪製之前同步執行。它會阻塞瀏覽器的繪製。
- 適用場景:
- useEffect:適用於絕大多數副作用,如數據請求、設置訂閱、計時器等,這些操作不需要在瀏覽器繪製前完成,且不希望阻塞用戶界面。
- useLayoutEffect:適用於需要在 DOM 更新後、瀏覽器繪製前讀取 DOM 佈局信息並同步觸發重新渲染的場景。例如,測量 DOM 元素的尺寸或位置,並根據這些信息更新狀態,以避免畫面閃爍。由於其同步阻塞特性,應謹慎使用,避免性能問題。
面試題目 2: setTimeout
and promise practice
- 考察重點: 結合實際場景理解 JavaScript 的事件循環 (Event Loop)、宏任務 (Macrotask) 和微任務 (Microtask) 的執行順序。
- 解析與答案:
- 這個題目通常會給出一段包含
setTimeout
, Promise (.then
,.catch
,async/await
) 的代碼,要求預測輸出順序。 - 事件循環 (Event Loop): JavaScript 引擎用來管理和執行異步任務的機制。
- 宏任務 (Macrotask/Task): 包括
script
(整體代碼)、setTimeout
,setInterval
,setImmediate
(Node.js), I/O 操作, UI rendering 等。宏任務會被放入宏任務隊列。 - 微任務 (Microtask/Job): 包括
Promise.then/catch/finally
,async/await
(其後的代碼),queueMicrotask
,MutationObserver
等。微任務會被放入微任務隊列。 - 執行順序:
- 執行當前的宏任務(例如 script 標籤內的同步代碼)。
- 執行過程中遇到的同步代碼立即執行。
- 遇到異步任務(如 setTimeout 或 Promise),將其回調函數註冊到對應的隊列(宏任務隊列或微任務隊列)。
- 當前宏任務執行完畢後,立即檢查微任務隊列。
- 執行所有當前微任務隊列中的任務。如果在執行微任務過程中又產生了新的微任務,會將其加入隊列末尾,並在本輪繼續執行,直到微任務隊列清空。
- (可選)執行 UI 渲染操作(瀏覽器環境)。
- 從宏任務隊列中取出一個任務執行,重複步驟 2-6。
setTimeout
vs Promise:setTimeout
的回調屬於宏任務,而 Promise (.then
,await
後的代碼) 屬於微任務。因此,在同一次事件循環中,Promise 的回調總是比setTimeout
的回調先執行。- 實踐練習 (示例代碼及輸出預測): JavaScriptconsole.log('script start'); // 1. 同步 setTimeout(function() { console.log('setTimeout'); // 5. 宏任務 }, 0); Promise.resolve().then(function() { console.log('promise1'); // 3. 微任務 }).then(function() { console.log('promise2'); // 4. 微任務 (由上一個 then 產生) }); console.log('script end'); // 2. 同步 // 預期輸出: // script start // script end // promise1 // promise2 // setTimeout
- 這個題目通常會給出一段包含
面試題目 3:git 分支的合併
- 考察重點: 理解 Git 中合併分支 (
git merge
) 的概念、常用策略以及如何處理合併衝突。 - 解析與答案:
- 目的: 將一個分支的更改整合到另一個分支中。通常是將開發完成的功能分支合併回主分支 (如
main
或master
)。 - 常用命令: Bash# 1. 切換到接收更改的目標分支 (e.g., main) git checkout main # 2. 執行合併命令,將來源分支 (e.g., feature-branch) 合併進來 git merge feature-branch
- 合併策略:
- Fast-forward (快進合併): 如果目標分支 (main) 在來源分支 (feature-branch) 創建後沒有任何新的提交,Git 會直接將 main 分支的指針移動到 feature-branch 的最新提交。不會創建新的合併提交記錄,保持線性歷史。
- Recursive (遞歸合併) / Three-way Merge (三方合併): 如果目標分支和來源分支各自都有新的提交(歷史產生了分叉),Git 會執行三方合併。它會找到兩個分支的共同祖先節點,並將兩個分支的更改與共同祖先進行比較,生成一個新的合併提交 (Merge Commit)。這個提交有兩個父提交。這是 Git 的默認合併策略(當不能快進時)。
- 合併衝突 (Merge Conflicts): 如果兩個分支修改了同一個文件的同一部分,Git 無法自動決定保留哪個更改,就會發生合併衝突。
- 標記: Git 會在衝突的文件中用特殊標記(<<<<<<<, =======, >>>>>>>)標示出衝突的部分。
- 解決: 需要手動編輯衝突文件,選擇要保留的代碼(可能來自一方,也可能是兩者結合,或者全新修改),然後移除特殊標記。
- 完成: 解決所有衝突後,使用 git add <resolved_file> 將文件標記為已解決,最後執行 git commit 來完成合併(如果是三方合併,Git 通常會預填合併信息,確認即可;如果是 rebase 中的衝突,則使用 git rebase --continue)。
- 目的: 將一個分支的更改整合到另一個分支中。通常是將開發完成的功能分支合併回主分支 (如
面試題目 4:Macrotask 與 MicroTask
- 考察重點: 同面試題目 2 (
setTimeout
and promise practice),深入理解事件循環、宏任務和微任務的定義、種類及執行順序。 - 解析與答案: (參考上面題目 2 的解析,這裡可以補充更多例子)
- 宏任務例子:
script
(整體代碼)、setTimeout
,setInterval
,requestAnimationFrame
(通常在渲染前執行,但行為類似宏任務), I/O, UI rendering。 - 微任務例子:
Promise.then/catch/finally
,queueMicrotask()
(顯式添加微任務),MutationObserver
回調,process.nextTick
(Node.js 環境,比 Promise 微任務優先級更高)。 - 關鍵點: 每次事件循環只執行一個宏任務,但會執行完所有的微任務。微任務提供了在當前宏任務結束後、下一個宏任務開始前(或 UI 渲染前)立即執行某些邏輯的能力,常用於確保某些操作在狀態更新後、渲染前完成。
- 宏任務例子:
面試題目 5:閉包 (Closure)
- 考察重點: 理解閉包的定義、形成條件、原理以及常見應用場景。
- 解析與答案:
- 定義: 閉包是指一個函數以及其周圍的狀態(詞法環境)的組合。換句話說,閉包讓你有權限從內部函數訪問外部函數的作用域。在 JavaScript 中,閉包會在你創建一個函數時,同時創建了該函數的作用域鏈。
- 形成條件: 當一個內部函數引用了其外部函數的變數(即使外部函數已經執行完畢),閉包就產生了。
- 原理: JavaScript 的詞法作用域規則決定了函數在定義時就能確定其能訪問哪些變數。即使外部函數執行完畢,其執行上下文可能銷毀,但如果內部函數仍然存在引用(例如被返回或賦值給全局變數),那麼外部函數的活動對象(包含其變數)就不會被垃圾回收機制回收,內部函數依然可以通過其作用域鏈訪問這些變數。
- 示例: JavaScriptfunction outerFunction() { let outerVariable = 'I am outside!'; function innerFunction() { console.log(outerVariable); // 訪問外部函數的變數 } return innerFunction; // 返回內部函數 } const myClosure = outerFunction(); // outerFunction 執行完畢,但 outerVariable 所在的環境被 innerFunction 引用 myClosure(); // 輸出: "I am outside!" -> 閉包的效果
- 應用場景:
- 數據封裝與私有變數: 模擬私有成員,保護內部狀態不被外部直接訪問。
- 回調函數與異步: 在回調函數中訪問定義它們時的上下文信息(例如,循環中為每個 setTimeout 保存特定的索引值)。
- 函數工廠: 創建具有特定配置或狀態的函數。
- 柯里化 (Currying) / 部分應用 (Partial Application)。
- 模塊化: 在 ES6 模塊出現之前,常用立即執行函數表達式 (IIFE) 配合閉包來創建模塊作用域。
第三站:Garena - 專注 React 性能優化
Garena 的面試題目似乎更側重於 React 的實際應用和性能優化技巧。
面試題目 1:useMemo
- 考察重點: 理解
useMemo
Hook 的作用、使用方法、依賴數組的重要性,以及它與useCallback
的區別。 - 解析與答案:
- 作用:
useMemo
用於記憶化 (Memoization) 一個計算結果。它接收一個「創建」函數和一個依賴數組。useMemo
僅在某個依賴項改變時才重新計算記憶化的值。這種優化有助於避免在每次渲染時都進行高開銷的計算。 - 語法: JavaScriptconst memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
- 第一個參數是一個函數,該函數的返回值會被記憶化。
- 第二個參數是一個依賴數組。只有當數組中的某個值發生變化時,第一個參數的函數才會重新執行。如果傳入空數組 [],函數只會在組件初始渲染時執行一次。
- 使用場景:
- 當組件中有計算成本很高的操作(例如,對大型數組進行複雜的過濾、排序或計算)時,可以使用 useMemo 來緩存結果,避免每次渲染都重新計算。
- 當需要將某個計算結果作為 prop 傳遞給子組件,並且希望僅在該結果實際變化時才觸發子組件的重新渲染(配合 React.memo 或 shouldComponentUpdate 使用)時。
- 與
useCallback
的區別: - useMemo:記憶化函數的返回值(一個值)。
- useCallback:記憶化函數本身(一個函數實例)。useCallback(fn, deps) 等價於 useMemo(() => fn, deps)。useCallback 主要用於將回調函數傳遞給經過優化的子組件(如使用 React.memo 包裹的組件)時,防止因為父組件渲染導致函數實例變化,從而引起不必要的子組件渲染。
- 作用:
總結與啟示
從 Pelith、Xrex 到 Garena 的面試經驗可以看出:
- 基礎是根本: 無論框架如何演進,對 JavaScript 核心(異步、事件循環、閉包、
typeof
等)的深刻理解始終是重中之重。 - 框架深度不可少: 對於 React 開發者而言,不僅要會用 Hook,更要理解其原理、區別和適用場景(
useEffect
vsuseLayoutEffect
,useMemo
vsuseCallback
)。 - 實踐與原理結合:
setTimeout
與 Promise 的結合考察了事件循環的實際應用;Git 合併則考察了版本控制的實操能力。 - 性能優化意識:
useMemo
的考察體現了對應用性能的關注,這是進階工程師的必備素養。
準備前端面試,不僅要刷題,更要深入理解每個知識點背後的原理和應用場景。希望這篇整理能為你的求職之路點亮一盞明燈!祝你面試順利!