
每年校招季各家公司筆試題目總會在學生群里被翻來覆去地討論。歡聚時代2018年校招的web前端A卷就是當時討論熱度很高的一份。我印象最深的一點是整張卷子并不靠偏題怪題來刁難人它的命題思路非常貼近一線開發(fā)場景——不考你背了多少API而是看你在寫頁面、寫JS的時候有沒有真正理解瀏覽器在替你做什么、JS引擎在怎么執(zhí)行你的代碼。這份卷子適合兩類人一是正在準備前端校招的應屆生可以把題目當成知識盲區(qū)對照表二是入行一兩年、想系統查漏補缺的前端工程師。整張卷子分為選擇題、簡答題和手寫編程題三大模塊覆蓋范圍基本落在四個方向JavaScript語言特性、瀏覽器工作原理、HTML/CSS布局、網絡協議與Web安全。下面我按這幾個方向把命題邏輯拆開結合我當時備考和實際面試的經驗來聊。1. 卷子結構與考察方向拆解1.1 整體命題邏輯為什么考這些不考那些先給沒參加過校招的朋友科普一下筆試設計的基本邏輯。校招筆試不是用來挑“最強天才”的而是用來做第一輪粗篩的。面試官一天要面幾十個人筆試必須用盡量少的題目把“基礎不牢靠”和“基礎扎實有潛力”這兩類人快速分開。所以在有限時間內命題人會優(yōu)先選擇覆蓋面廣、區(qū)分度高的知識點而不是某個小眾框架的上層用法。歡聚時代這份A卷的題型分布在當年很有代表性模塊題型題量占比考察目標JavaScript語言特性單選/多選/手寫約40%語法理解、運行機制、編碼能力瀏覽器原理單選/簡答約20%渲染過程、緩存、異步事件HTML/CSS布局單選/手寫約20%布局能力、盒模型、選擇器網絡與安全單選/簡答約10%HTTP協議、緩存、跨域與XSS算法基礎手寫約10%數組/字符串處理、邏輯思維從這個配比能看出來JavaScript是絕對核心這也是正常的。前端崗位的日常工作本質就是跟JS打交道框架可以換語言層面的功底騙不了人。瀏覽器原理占20%因為它決定了你對線上問題的排查能力——為什么頁面白屏、為什么滾動卡頓、為什么資源重復加載這些都在瀏覽器原理的知識范疇里。歡聚時代本身是做直播和音視頻業(yè)務的對實時交互體驗要求很高。所以這份卷子里異步編程、事件循環(huán)、性能優(yōu)化相關的題占比明顯高于一般公司。這也是命題邏輯里很有意思的一層企業(yè)的業(yè)務形態(tài)會直接影響考點側重。備考時候如果能提前了解目標公司的業(yè)務方向大致能猜出它更重視哪一塊。1.2 難度定位基礎題的“細節(jié)陷阱”A卷的整體難度屬于當年的主流水平沒有特別刁鉆的題但細節(jié)題很密。什么叫細節(jié)題就是看起來每個選項都認識但考的是你平時會不會真正去思考的那些點。舉個例子JS基礎經常出現這種題判斷typeof null、typeof function(){}、typeof []分別是什么。如果你平時寫代碼沒想過這類問題光靠感覺答題很容易在“object”和“function”之間猶豫。再比如0.1 0.2 0.3的結果為什么是false這背后是浮點數精度問題屬于計算機基礎范疇但很多前端同學直到工作兩三年都沒搞明白。細節(jié)題的價值在于它不是死記硬背能答好的它反映的是候選人平時會不會深入思考。面試官真正想看的不是你記住了多少結論而是你有沒有建立“遇到問題追到原理層面”的習慣。2. JavaScript核心考點扎實功底是分水嶺2.1 閉包、作用域與this指向必考且拉開差距JavaScript這部分幾乎可以確定會出現閉包和作用域的題。最經典的代碼題長這樣for (var i 0; i 5; i) { setTimeout(function() { console.log(i); }, 100); }問輸出結果是什么。答案不是0、1、2、3、4而是連續(xù)5個5。考點有兩個一是var沒有塊級作用域循環(huán)里創(chuàng)建的i是同一個變量二是setTimeout的回調是宏任務要等同步代碼執(zhí)行完才運行而那時循環(huán)已經結束i已經變成5。這道題的改法在當年是手寫題的常客改成let聲明或者用閉包傳參for (var i 0; i 5; i) { (function(j) { setTimeout(function() { console.log(j); }, 100); })(i); }用IIFE把每次循環(huán)的i值作為參數固化到閉包里。后來ES6普及l(fā)et成了首選方案。但筆試如果只寫let建議把原理講清楚單純換成let說明你只是知道結論不一定理解背后的作用域機制。this指向的題也幾乎是必考的。常見考法var name window; var obj { name: obj, getName: function() { return this.name; } }; var fn obj.getName; console.log(fn()); console.log(obj.getName());第一行輸出window第二行輸出obj。原因在于this的綁定規(guī)則誰調用函數this就指向誰。fn()是全局調用this指向windowobj.getName()是對象方法調用this指向obj。當年還有一道擴充題把所有情況串起來普通函數調用、對象方法調用、call/apply/bind調用、new調用、箭頭函數。每寫一個調用方式后面就跟一個輸出結果。這類題考察的是你對this綁定優(yōu)先級有沒有完整的理解——new bind call/apply 對象方法調用 默認綁定。箭頭函數則特殊它沒有自己的this往外層作用域找。2.2 原型鏈與繼承手寫題的決定性題目原型和繼承是2018年前后前端筆試的“壓軸常客”。選擇題考instanceof的判斷結果手寫題大概率讓你實現一個繼承。先看選擇題經典function Parent() { this.name parent; } function Child() { this.age 1; } Child.prototype new Parent(); var child new Child(); console.log(child instanceof Parent); console.log(child instanceof Child); console.log(child.constructor Parent);前兩個輸出都是true因為child的原型鏈上既有Child.prototype也能通過Child.prototype內部的[[Prototype]]找到Parent.prototype。第三個容易丟分child.constructor此時指向的是Parent。原因在于Child.prototype被整個重新賦值為new Parent()這個對象身上的constructor屬性繼承自Parent.prototype指向Parent。手寫繼承題當時最穩(wěn)妥的回答是“組合繼承”和“寄生組合繼承”。我建議優(yōu)先掌握寄生組合繼承它兼容了原型鏈繼承和構造函數繼承的優(yōu)點也是ES6 class繼承在Babel轉譯后的實際實現方式function Parent(name) { this.name name || parent; this.colors [red, blue]; } Parent.prototype.sayHello function() { console.log(Hello this.name); }; function Child(name, age) { Parent.call(this, name); this.age age; } Child.prototype Object.create(Parent.prototype); Child.prototype.constructor Child; Child.prototype.sayAge function() { console.log(this.age); };這里核心是Object.create(Parent.prototype)它創(chuàng)建一個以父類原型為新對象原型的對象再賦給Child.prototype。這樣避免了直接new Parent()帶來的多余屬性也保證了instanceof判斷正確。最后一定要補Child.prototype.constructor Child修復constructor指向否則后面判斷對象構造函數會出問題。2.3 事件循環(huán)與異步機制直播業(yè)務公司的重點題前面提到歡聚時代做直播異步處理在業(yè)務里非常重要。所以這套卷子里事件循環(huán)相關的輸出題占比不低而且難度逐年上升。基礎題是經典的宏任務/微任務輸出順序console.log(A); setTimeout(function() { console.log(B); }, 0); Promise.resolve().then(function() { console.log(C); }); console.log(D);輸出順序是A、D、C、B。原因是同步代碼先全部執(zhí)行完然后執(zhí)行微任務隊列Promise.then回調最后才是宏任務隊列setTimeout回調。這個知識點在當年很多人會搞反以為setTimeout的0毫秒是立即執(zhí)行實際上0毫秒只是表示“盡快加入宏任務隊列”并不能插隊到微任務前面。進階版會疊加async/awaitasync function test() { console.log(start); await Promise.resolve(); console.log(end); } test(); console.log(sync);這里的輸出需要理解await的語義。await右側的表達式會立即執(zhí)行遇到await時test函數暫停后續(xù)代碼作為微任務排隊。所以輸出是start、sync、end。遇到這類題我建議在草稿紙上畫一下任務隊列先寫同步任務再畫一個微任務隊列和一個宏任務隊列遇到Promise.then和await后面代碼就往微任務隊列塞遇到setTimeout就往宏任務隊列塞。按這個流程走基本不會錯。而且這個思路本身也是實際排查線上問題時需要的能力——比如為什么某個接口回調比另一個后執(zhí)行為什么圖表渲染在滾動事件之后才更新本質都是任務隊列的順序問題。3. 瀏覽器原理與網絡一線開發(fā)的基本功3.1 從輸入URL到頁面渲染簡答題的經典框架這類簡答題幾乎每個前端筆試都會出現A卷也不例外。題目一般長這樣“描述在瀏覽器地址欄輸入網址并回車到頁面完整顯示中間經歷了哪些步驟”完整的回答框架是URL解析瀏覽器判斷輸入是合法URL還是搜索關鍵詞補全協議和路徑。DNS解析把域名解析成IP地址依次查找瀏覽器緩存、系統緩存、路由器緩存、根DNS服務器。建立TCP連接三次握手客戶端發(fā)送SYN、服務器返回SYNACK、客戶端回復ACK。發(fā)起HTTP請求構造請求行、請求頭、請求體通過TCP發(fā)送。服務器處理并返回后端處理請求返回響應報文和響應體。瀏覽器解析渲染解析HTML構建DOM樹、解析CSS構建CSSOM樹、兩者合成渲染樹、計算布局、繪制并合成圖層。斷開連接短連接四次揮手或者保持長連接復用。我當時復習這道題時專門整理了一段口訣“解析域名、建立連接、發(fā)送請求、解析響應、構建渲染、執(zhí)行腳本”。回答時要注意兩點一是有邏輯層次從網絡層面到渲染層面逐步展開二是要提到關鍵細節(jié)比如渲染過程中遇到script標簽會阻塞DOM解析所以腳本一般放body底部或加defer/async。這個細節(jié)在筆試里很容易被忽略但恰恰是面試官判斷你“真的理解”還是“背過答案”的分界線。3.2 回流重繪與性能優(yōu)化面試加分項渲染原理延伸出來的高頻題就是回流reflow/layout和重繪repaint。筆試選擇題會這么考以下哪些操作會觸發(fā)回流選項一般包括修改元素寬度、修改顏色、讀取offsetWidth、刪除DOM節(jié)點、改變窗口大小、添加類名。這里有個容易踩坑的點很多人以為“讀取”不會觸發(fā)回流但offsetWidth、clientWidth、getComputedStyle這類讀取操作會強制瀏覽器同步完成布局計算然后才返回值。原因是瀏覽器為了性能會合并多個樣式修改延遲一次統一回流但當你讀取布局屬性時它沒辦法給個“過期”的值只能先算一遍。關于回流的優(yōu)化手段我在實際項目中驗證有效的有幾條把需要多次修改的DOM操作合并用document.createDocumentFragment()或者一次性改class。用transform代替top/left做動畫因為transform走合成器不觸發(fā)回流重繪。對需要高頻觸發(fā)的事件如scroll、resize做防抖或節(jié)流。需要讀取布局屬性時盡量一次性讀取緩存不要反復讀。用will-change或contain屬性減少渲染范圍。這些優(yōu)化思路在筆試里可以作為簡答題的補充答案在面試中則是實打實的加分項。尤其是“transform為什么比top高性能”這個追問能答出“合成器在GPU上處理不經過布局和繪制”就算過關。3.3 HTTP緩存機制與狀態(tài)碼細節(jié)決定成敗網絡協議部分A卷側重緩存機制。核心考點是強緩存和協商緩存的完整鏈路。強緩存有兩個頭字段ExpiresHTTP/1.0和Cache-ControlHTTP/1.1。Cache-Control: max-age3600表示從請求時刻起緩存1小時。而Expires是絕對時間如果客戶端時間和服務器時間不一致緩存就會失效所以現代項目基本以Cache-Control為主。協商緩存也有兩個頭字段Last-Modified/If-Modified-Since按文件修改時間判斷ETag/If-None-Match按文件內容指紋判斷。兩者區(qū)別在于修改時間只能精確到秒同一秒內多次修改就判斷不出來ETag是內容哈希更精確但計算成本更高。服務器返回304表示“你可以用本地緩存”這次請求不返回響應體。緩存題還有一種變體無緩存刷新CtrlF5和普通刷新F5有什么區(qū)別普通刷新會帶上If-Modified-Since或If-None-Match走協商緩存強制刷新則會繞過強緩存和協商緩存直接向服務器發(fā)新請求。狀態(tài)碼的考點集中在200成功、301永久重定向、302臨時重定向、304緩存命中、403禁止訪問、404找不到資源、500服務器內部錯誤、502網關錯誤。還有個容易混淆的301和302的區(qū)別一個是永久遷移搜索引擎會更新鏈接權重一個是臨時跳轉。4. CSS/HTML應用題布局細節(jié)最容易翻車4.1 經典布局的實現與對比CSS部分A卷還是老套路盒模型、居中布局、兩欄/三欄布局、flex布局。這些題目本身不難但很多人因為平時寫代碼依賴框架自己手寫的時候反而不利索。盒模型的考點是標準盒模型和IE盒模型的區(qū)別。標準盒模型的width只包含內容區(qū)padding和border是額外加上的IE盒模型的width包含內容、padding和border。box-sizing: border-box就是把元素設置為IE盒模型這在移動端布局里非常實用因為子元素設置百分比寬度后再加padding不會超出父容器。垂直水平居中方案是手寫題常客。當年我給出的標準答案至少要有四種/* 方案一flex */ .parent { display: flex; justify-content: center; align-items: center; } /* 方案二絕對定位 transform */ .parent { position: relative; } .child { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } /* 方案三margin: auto 配合絕對定位 */ .child { position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; } /* 方案四table-cell */ .parent { display: table-cell; text-align: center; vertical-align: middle; }筆試要求寫兩到三種面試則希望你能說出每種方案的局限性和適用場景。比如flex方案要求父容器高度確定絕對定位transform要求父容器是定位父級table-cell適合舊瀏覽器兼容。能講出這些才算真正掌握了。三欄布局是CSS布局題里的“定番題”。圣杯布局和雙飛翼布局的思路都是左右兩欄定寬、中間自適應區(qū)別在于圣杯用padding騰位置雙飛翼用中間欄內部的margin騰位置。我當時練習時做了個判斷雙飛翼的邏輯更直接不容易寫錯但兩者原理都要會說。2023年以后flex和grid已經全面普及這類傳統方案在筆試中的出現頻率有所下降但一旦出現如果答不上來會比較尷尬。4.2 選擇器優(yōu)先級與BFC細節(jié)里的高分題選擇器優(yōu)先級計算是選擇題高發(fā)區(qū)。記一個權重模型內聯樣式1000id選擇器100類/屬性/偽類10元素/偽元素1。比較時按位相加大的優(yōu)先。!important優(yōu)先級最高但慎用因為它會破壞級聯規(guī)則后期維護很難覆蓋。BFC塊級格式化上下文是這一部分的另一個考點。選擇題一般問你哪個屬性會觸發(fā)BFC或者BFC能解決什么問題。觸發(fā)BFC的條件有根元素、float不為none、position為absolute或fixed、display為inline-block或table-cell、overflow不為visible、display: flow-root。BFC解決的核心問題有三個清除浮動子元素浮動導致父容器高度塌陷時給父容器創(chuàng)建BFC它能包含浮動子元素。防止margin合并垂直方向上相鄰元素的margin會合并把其中一個元素包進BFC可以阻止合并。阻止元素被浮動元素覆蓋BFC區(qū)域不會與浮動元素重疊。我當時手寫過一段清除浮動的兼容寫法.clearfix::after { content: ; display: block; clear: both; }這個方案比給父容器加overflow: hidden更安全因為后者在某些場景下會裁掉下拉菜單之類的溢出內容。筆試簡答題里能寫出這種細節(jié)是加分項。5. 手寫編程題解題過程與邊界處理5.1 防抖與節(jié)流必考手寫題要注意邊界手寫題第一道高頻題就是防抖debounce和節(jié)流throttle。題目有時候只讓你寫“實現防抖函數”有時候更雞賊要求“實現防抖函數并說明它和節(jié)流的區(qū)別”。防抖的核心邏輯是事件觸發(fā)后設定一個等待時間等待時間內再次觸發(fā)則重新計時只有停止觸發(fā)后才執(zhí)行一次。function debounce(fn, delay) { let timer null; return function(...args) { if (timer) clearTimeout(timer); timer setTimeout(() { fn.apply(this, args); timer null; }, delay); }; }我當時寫這段時容易漏兩個點一是this的綁定問題回調里直接用fn()會導致this丟失必須用fn.apply(this, args)把this傳進去二是clearTimeout之后timer要置null防止連續(xù)調用次數的累積污染。節(jié)流的核心邏輯是固定時間間隔內只執(zhí)行一次。function throttle(fn, interval) { let last 0; return function(...args) { let now Date.now(); if (now - last interval) { last now; fn.apply(this, args); } }; }還有一種用定時器實現的版本區(qū)別在于末尾是否多執(zhí)行一次。筆試時如果能寫出“第一次觸發(fā)立即執(zhí)行結束后再補充執(zhí)行一次”的版本并解釋兩種時間的差異基本就是滿分答案。5.2 數組去重與扁平化一題多解展示功底數組去重是另一道高頻手寫題。直接用Set是最簡潔的const unique (arr) [...new Set(arr)];但只寫一行可能讓面試官覺得你只會API所以建議同時寫出filter加indexOf的方案const unique (arr) arr.filter((item, index) arr.indexOf(item) index);這道題想拿高分關鍵在于主動說清楚不同方案的適用場景。比如Set方案無法區(qū)分1和1其實不是Set使用SameValueZero比較1和1是不同的值NaN會被正確去重這是它比indexOf方案強的地方。但如果數組元素是對象Set做的是引用比較無法按對象內容去重這時候需要自己寫reduce加Map組合方案。數組扁平化也是一道經典題。ES6有Array.prototype.flat(depth)但手寫實現要考察遞歸能力function flatten(arr) { return arr.reduce((prev, cur) { return prev.concat(Array.isArray(cur) ? flatten(cur) : cur); }, []); }注意遞歸方案對嵌套特別深的數組可能導致調用棧溢出。如果面試追問可以用棧或迭代來優(yōu)化把深度控制在O(n)空間。我在寫這段時會把concat和展開運算符[...prev, ...]的性能差異也提一下這個細節(jié)能體現你對代碼性能的敏感度。5.3 手寫發(fā)布訂閱Event Bus的常規(guī)操作發(fā)布訂閱模式是這類筆試里比較有區(qū)分度的手寫題。題目通常說實現一個簡單的EventEmitter支持on、once、off、emit四個方法。class EventEmitter { constructor() { this.events new Map(); } on(name, fn) { if (!this.events.has(name)) { this.events.set(name, []); } this.events.get(name).push(fn); } once(name, fn) { const wrapper (...args) { fn.apply(this, args); this.off(name, wrapper); }; this.on(name, wrapper); } off(name, fn) { if (!this.events.has(name)) return; if (!fn) { this.events.delete(name); return; } const fns this.events.get(name); const index fns.indexOf(fn); if (index -1) fns.splice(index, 1); } emit(name, ...args) { const fns this.events.get(name); if (fns fns.length) { fns.slice().forEach(fn fn.apply(this, args)); } } }這題有幾個容易丟分的細節(jié)once注冊的回調執(zhí)行后要立即解綁所以外面包了一層wrapperoff時必須能用wrapper從列表中刪除emit時遍歷回調數組要用slice()淺拷貝一份防止回調內部調用off導致正在遍歷的數組被修改off如果不傳具體函數應該清空該事件的所有回調。這些邊界情況在筆試里不一定要求全部都寫出來但能在白紙上寫完整說明你的工程意識是有的。6. 筆試常見丟分點與復習路線建議6.1 我觀察到的三大丟分原因先說丟分點。我身邊參加過這份筆試的同學回來說得最多的不是“題不會”而是“會但是沒寫全”。這與校招筆試的評分方式密切相關手寫題往往是按點給分的少了邊界處理、少了原理說明扣分都很正常。第一個丟分點是手寫題沒有處理邊界條件。比如實現防抖函數時不處理this綁定實現EventEmitter時不考慮once回調的自動解綁數組去重時忽略NaN的去重需求。這些邊界情況在真實項目中很重要筆試評分時也是區(qū)分“背過模板”和“真正理解”的關鍵判據。第二個丟分點是選擇題里模棱兩可的選項過于糾結導致時間分配失衡。一張卷子選擇題大概30道建議每道不超過1分半不會的先跳過把時間留給手寫題。手寫題分值高、區(qū)分度大寫不全基本就等于丟了這部分分數。第三個丟分點是簡答題只寫條目不解釋。問“瀏覽器渲染過程”如果只寫“構建DOM樹、構建CSSOM樹、合成渲染樹”三行分數一定不高。閱卷人想看的是你對每一步的理解比如構建DOM樹遇到script怎么處理、CSSOM樹會不會阻塞渲染、合成樹和渲染樹的區(qū)別。解釋得越細越能證明你真的做過頁面性能排查。6.2 針對性的復習路線從筆試到面試的延續(xù)備考前端校招我的建議是分三條線并行走。第一條線是語言基礎。把JavaScript的核心機制過一遍重點不是背API而是理解原理作用域鏈、閉包、原型鏈、this綁定規(guī)則、async/await和Promise的執(zhí)行順序。推薦的方式是每個知識點都自己寫一個小例子在控制臺驗證輸出再嘗試解釋原因。第二條線是瀏覽器和網絡。推薦動手做一個“輸入URL到頁面渲染”的完整實驗在網絡面板里觀察強緩存和協商緩存的請求頭變化打開Performance面板看回流重繪和長任務。這些實踐能幫你把抽象概念和真實現象對應起來。第三條線是刷手寫題。防抖節(jié)流、數組去重、數組扁平化、實現Promise、深拷貝、EventEmitter這些是前端筆試的常青題目。我的建議是每個題至少手寫三遍第一遍看答案后默寫第二遍閉卷獨立寫第三遍嘗試優(yōu)化和擴展。還有一條面試層面的建議筆試題目通常會在面試中被追問。答完一道手寫題面試官很可能會問“這段代碼有什么問題”“如果并發(fā)調用會怎么樣”“內存方面有沒有考慮”。所以復習時不要只滿足于寫出來要有意識地思考代碼的邊界和優(yōu)化空間。寫在最后回看歡聚時代這份2018年的web前端A卷我覺得它最大的參考價值不是題目本身而是它對基礎知識的重視程度。很多人在校招季容易陷入一種焦慮拼命追最新框架、最新特性卻被一張考察閉包和事件循環(huán)的卷子教做人。事實是技術棧會變框架會過時但JavaScript語言的底層機制、瀏覽器的工作原理、HTTP協議的交互邏輯這些才是前端工程師真正的護城河。如果你正在準備校招建議把這份卷子當作一面鏡子逐個考點自查哪一塊不熟悉就補哪一塊。我在實際備考中感受到最有效的學習方式就是把每一個考點講給別人聽講不清楚的地方就是你的盲區(qū)。希望這份拆解能幫你少走一些我當時走過的彎路。