很多重視技術的網際網路公司在工程師招聘的技術面環節都會要求候選人在紙上寫程式碼(後文用“紙上程式碼”代稱),面試官想通過這種方式考察哪些點?候選人該注意哪些點?本文基於美團早幾年常用的一道區分度比較高的面試題來做詳細講解,這道題我現在還在用,面過的人很多,但是紙上程式碼環節能答到滿分的少之又少。
共 3655 字,讀完需 7 分鐘。本文為《破解前端面試》系列文章的第 3 篇,前 2 篇連結在這裡:閉包篇、DOM 篇。
為什麼要紙上程式碼?
紙上程式碼(也有可能在白板上寫)的做法乍看起來不夠人性,但如果你是團隊的 Leader,什麼樣的人能更好的融入團隊?如果你是老闆,你願意掏錢養什麼樣的員工?紙上程式碼的基本目的就是考察候選人是否具備出活的能力,附帶考察候選人是否思路靈活、知識面廣。
紙上程式碼環節怎麼考察出活的能力?首先是出活的速度,沒有編碼基本功的人快速出活的概率是極低的,100% 依賴百度或者 IDE 自動完成才能完成基本任務的工程師算不上合格的工程師;其次是出活的質量,通過編碼過程可以瞭解候選人通過學習和訓練積累下來的編碼風格、思考方法等;此外,通過紙上程式碼也可以瞭解候選人接受和完成任務的主動性,是不是願意接受任何團隊需要完成的任務。
某種程度上說,紙上程式碼過程就是今後工作的縮影,既然如此,面試時排練下不是挺好的麼?
紙上程式碼該怎麼做?
通常來說,紙上程式碼都不會問特別複雜的問題,很可能只是完成非常通用的需求,解決實際遇到的業務問題,或者用某種語言實現某種演算法。在提出實際業務問題的程式碼題之前,面試官會通過部分前置問題了解候選人對解決業務問題所需知識的掌握程度,並在必要的情況下給出知識補充。
比如,前文提到的那道美團的程式碼題是:不借助第三方庫的條件下,用 JS 編寫函式從下面的 URL 串中解析出所有的引數:
http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&d&enabled複製程式碼
期望的返回結果格式如下:
{
user: 'anonymous',
id: [123, 456], // 重複出現的 key 要組裝成陣列,能被轉成數字的就轉成數字型別
city: '北京', // 中文
enabled: true, // 未指定值的 key 約定值為 true
}複製程式碼
對於使用過 Node.js 中的 querystring
或者社群中的 qs
、uri.js
模組的同學對這個可能再熟悉不過了,而那些不熟 HTTP GET 請求引數攜帶方式的候選人也不用著急,因為這種情況下面試官會解釋 URL 引數的構造規則,至於對網路知識的掌握程度,是另外的關注點了。實際操作中,在我拿出這個問題之前,已經跟候選人聊了比較多的 HTTP 話題了。
1. 開始動手前
相當比例的候選人拿到問題,會立即提筆開始寫程式碼,這是面試官最不願看到的,和學校考試的填空題不同,紙上程式碼作為綜合素質環節,面試官希望看到全面的你,如果工作中也是這樣拿到需求不分青紅皁白就開搞,最終的結果可能常常是事倍功半。
謀定而後動,動手前一定要搞清楚問題。怎樣才算是把問題搞清楚了?要清楚輸入的特徵,是否會出現各種奇怪的輸入(腦子裡面有這根弦的人通常不會差,但是面試官會小心求證,看看你能想到哪些);要清楚對解決辦法的其他約束條件,比如時間複雜度,空間複雜度。而搞清楚問題的方法就是追問面試官,比如,針對上面的程式碼,可以追問的問題:
- 未指定值的 key 是否會重複出現?如果重複出現該怎麼處理?
- 數字中只包含整數?是否包含浮點數?科學計數法?
- 對程式碼的效能要求是怎樣的?提出這個問題的時候,候選人心中可能已經有多重方法了。
就如同在實際工作中接需求的時候,需要知道需求的邊界,各種可能的特殊情況,合作方對於排期的期望,需求中各個要點優先順序界定,從決策論的角度來看,掌握更充分的資訊,才能讓你對技術複雜度、需求排期有更合理的預估,避免在做到一半或做完的時候發現與實際需求不符。
搞清楚問題之後,相信你心中已經有了基本思路,不過動手的時機還沒到,你應該把思路介紹給面試官,確認自己是否自己是否忽略了某些要點,這也是展示溝通能力的好機會,知道什麼是有效溝通的同學應該能明白接收資訊後向信源確認的重要性。
需要注意的是,質疑精神強烈的同學在動手前會提很多問題,看起來是好事情,但如果只是停留在質疑層面,不願意動手,留給面試官的印象就會是你是個挑活的人。在我的招聘經歷中就曾遇到過因為覺得程式碼題要解決的問題沒有任何意義而拒絕寫程式碼的人,我沒辦法只能客氣的把他送走。因為,對不認同事物的寬容程度很低的人很容易給團隊帶來壞味道。
確定了問題邊界和解決問題的思路,接下來你可以開始動手編碼。
2. 編碼過程中
解決 QueryString 引數解析問題的思路有好多種,比如字串線性遍歷法、字串分割法、正規表示式方法,在我面過的人中,用字串分割法的人最多,下面的討論我們就圍繞這種方法展開。線性遍歷法的實現可以參考 Node.js 內建的 querystring 模組。
編碼過程中需要考慮哪些要素呢?下面用具體的例子來分析,比如我經常拿到這樣的結果程式碼:
function parse(str) {
var obj = {};
var ary = str.split('&');
for (var i = 0; i < ary.length; i++) {
var tmp = ary[i].split('=');
if (!obj[tmp[0]]) {
obj[tmp[0]] = tmp[1] || true;
} else {
var tmp2 = [obj[tmp[0]], tmp[1] || true];
obj[tmp[0]] = tmp2;
}
}
return obj;
}複製程式碼
看到這樣的程式碼,相信你也已經皺起了眉頭,這段程式碼在表層、邏輯嚴謹性、健壯性都存在問題,更嚴重的是沒有滿足數值型引數的需求,透過這段程式碼也可以推斷候選人大概率是個不善於學習的人。
表層問題
表層問題主要指程式碼可讀性,評價標準是:是否看起來簡潔?是否看一眼就能理解它在做什麼?上面的結果有哪些具體的表層問題呢?
- 可讀性方面,如果你想在迴圈體裡面要追蹤解析到的鍵值對,需要在大腦中保持對映
key = tmp[0]
,value = tmp[1]
; - 變數命名方面,比如 tmp 的多次使用,ary 代稱陣列雖然也可以,社群中用 arr 比較多,變數命名多用約定俗成的會更好;
做了表層改進的參考程式碼如下:
function parse(str) {
var paramObj = {};
var paramArr = str.split('&');
for (var i = 0; i < paramArr.length; i++) {
var tmp = paramArr[i].split('=');
// 把 key 和 value 單獨拆開來,會清晰很多
var key = tmp[0];
var value = tmp[1] || true;
if (!paramObj[key]) {
paramObj[key] = value;
} else {
var newValue = [paramObj[key], value];
paramObj[key] = newValue;
}
}
return paramObj;
}複製程式碼
邏輯問題
邏輯不嚴謹的程式碼在不同輸入情況下的結果是不穩定的,具體表現為:
obj[tmp[0]]
不能正確判斷結果中是否已經存在某個 key,因為可能出現值為 0 的情況;- 上面的程式碼不能正確處理重複出現 2 次以上的 key,部分候選人到面試結束還沒想明白為啥;
- 按照規範,URL 中的的各種引數需要在 encode 之後拼接到 URL 中,對應的解析時需要 decode;
解決掉邏輯問題的參考程式碼如下:
function parse(str) {
var paramObj = {};
var paramArr = decoeURI(str).split('&'); // 先解碼
for (var i = 0; i < paramArr.length; i++) {
var tmp = paramArr[i].split('=');
var key = tmp[0];
var value = tmp[1] || true;
if (typeof paramObj[key] === 'undefined') { // 判斷 key 是否存在
paramObj[key] = value;
} else {
var newValue = Array.isArray(paramObj[key]) ? paramObj[key] : [paramObj[key]]; // 正確處理陣列
newValue.push(value);
paramObj[key] = newValue;
}
}
return paramObj;
}複製程式碼
健壯問題
整段程式碼沒有做任何的防禦性程式設計,會讓它很容報錯,哪些地方該做防禦性程式設計是值得拿捏的問題。QueryString 解析函式至少要要求自己的引數是字串吧?在函式開頭增加如下程式碼會更好:
//...
if (typeof str !== 'string') {
return {};
}
//...複製程式碼
需求問題
程式碼中沒有對數字做任何處理,拿到問題就埋頭寫程式碼的候選人幾乎都有這個問題,這個問題的考點是怎麼把能轉換成數字的值轉成數字。你想好怎麼做了麼?用 parseInt
?還是用 parseFloat
?
下面是能正確處理數字的參考程式碼:
function parse(str) {
if (typeof str !== 'string') {
return {};
}
var paramObj = {};
var paramArr = decodeURI(str).split('&');
for (var i = 0; i < paramArr.length; i++) {
var tmp = paramArr[i].split('=');
var key = tmp[0];
var value = tmp[1] || true;
// 處理數字:很多人忽略這裡的型別判斷,布林值傳給 Number 也會解析出數字
if (typeof value === 'string' && isNaN(Number(value)) === false) {
value = Number(value);
}
if (typeof paramObj[key] === 'undefined') {
paramObj[key] = value;
} else {
var newValue = Array.isArray(paramObj[key]) ? paramObj[key] : [paramObj[key]];
newValue.push(value);
paramObj[key] = newValue;
}
}
return paramObj;
}複製程式碼
不算問題的問題
下面兩點不算是問題,但是如果候選人能做到,無疑是加分項。
- 在 ES6 成為新語言標準的情形下,候選人還在大量的使用 var,雖然並沒有錯,但是你要有沒有更好的方法;
- 可以用更語義化的 JS 陣列方法來組織程式碼,比如 map、reduce,如果你知道的化,在面試中可以大膽使用;
使用 ES6 編寫的參考程式碼如下:
function parse(str) {
if (typeof str !== 'string') {
return {};
}
return decodeURI(str).split('&').map(param => {
const tmp = param.split('=');
const key = tmp[0];
let value = tmp[1] || true;
if (typeof value === 'string' && isNaN(Number(value)) === false) {
value = Number(value);
}
return { key, value };
}).reduce((params, item) => {
const { key, value } = item;
if (typeof params[key] === 'undefined') {
params[key] = value;
} else {
params[key] = Array.isArray(params[key]) ? params[key] : [params[key]];
params[key].push(value);
}
return params;
}, {});
}複製程式碼
此外,關注前端技術進展的同學可能會注意到部分現代瀏覽器提供了 URLSearchParams 的支援,可以用這個特性 1 行程式碼就搞定需求。
3. 編碼結束後
程式碼初版寫完之後,不要著急馬上展示給面試官,就像是需求開發完,你至少得自己先按需求文件走一遍,把程式碼原題中的輸入代進自己的程式碼做推演和簡單的邊界測試,然後再對著程式碼和麵試官講解。不出意外的話,推演過程你自己會發現部分問題,或者明顯的改進點,這些內容你都可以跟面試官提出來,因為這也是展示你的能力的機會。
總結
感謝你花時間讀到這裡,相信你已經理解了通過紙上程式碼的過程和結果可以深入考察候選人的基本素質、工作方式、出活能力,也知道了在解答程式碼題的不同環節該注意哪些要點:動手前搞清楚問題;編碼時注意編碼風格、邏輯嚴謹性、程式健壯性;編碼後要先自己測試和推演。當然,如果你之前沒注意到這些,需要接下來工作中多加練習。最後祝你能找到你想要的工作。
One More Thing
本文作者王仕軍,商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。如果你覺得本文對你有幫助,請點贊!如果對文中的內容有任何疑問,歡迎留言討論。想知道我接下來會寫些什麼?歡迎訂閱我的掘金專欄或知乎專欄:《前端週刊:讓你在前端領域跟上時代的腳步》。
Happy Hacking