很多重視技術的互聯網公司在工程師招聘的技術面環節都會要求候選人在紙上寫代碼(后文用“紙上代碼”代稱),面試官想通過這種方式考察哪些點?候選人該注意哪些點?本文基于美團早幾年常用的一道區分度比較高的面試題來做詳細講解,這道題我現在還在用,面過的人很多,但是紙上代碼環節能答到滿分的少之又少。
本文為《前端面試》系列文章的第 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. 編碼結束后
代碼初版寫完之后,不要著急馬上展示給面試官,就像是需求開發完,你至少得自己先按需求文檔走一遍,把代碼原題中的輸入代進自己的代碼做推演和簡單的邊界測試,然后再對著代碼和面試官講解。不出意外的話,推演過程你自己會發現部分問題,或者明顯的改進點,這些內容你都可以跟面試官提出來,因為這也是展示你的能力的機會。
總結
感謝你花時間讀到這里,相信你已經理解了通過紙上代碼的過程和結果可以深入考察候選人的基本素質、工作方式、出活能力,也知道了在解答代碼題的不同環節該注意哪些要點:動手前搞清楚問題;編碼時注意編碼風格、邏輯嚴謹性、程序健壯性;編碼后要先自己測試和推演。當然,如果你之前沒注意到這些,需要接下來工作中多加練習。最后祝你能找到你想要的工作。
轉載請注明: 文章轉載自:愛思資源網 http://www.randomwithlife.com/show-66-1104-1.html