今天在認真幹(劃)活(水)的時候,看到羣裏有人發了一道頭條的面試題,就順便看了一下,發現挺有意思的,就決定分享給你們,而且給出個人解決方案和思考過程。javascript
題目以下:vue
實現一個get函數,使得下面的調用能夠輸出正確的結果java
const obj = { selector: { to: { toutiao: "FE Coder"} }, target: [1, 2, { name: 'byted'}]}; get(obj, 'selector.to.toutiao', 'target[0]', 'target[2].name'); // [ 'FE Coder', 1, 'byted']
乍眼一看,這不就是實現一個lodash.get方法嗎?看上去好像很簡單。因此我就開始寫了第一個版本。思想其實很簡單,遍歷傳進來的參數,使用split將每個參數分隔開,而後遍歷取值,最終返回結果。git
function get(data, ...args) { return args.map((item) => { const paths = item.split('.'); let res = data; paths.map(path => res = res[path]); return res; }) }
一運行,果不其然,報錯了。github
後來仔細看了一下提供的測試代碼,發現竟然有target[0]這種東西。。竟然還帶了個數組索引。面試
冷靜分析一下,對於後面帶了個索引的類型,好比'target[0]',咱們確定是要特殊對待的。因此,咱們首先得先識別到這種特殊的類型,而後再對它進行額外處理。正則表達式
這個時候,很快的就能夠想到使用正則表達式來作這個事情。爲何呢?由於像這種帶有索引的類型,他們都有一個特點,就是有固定的格式:[num],那麼咱們只須要能構造出能夠匹配這種固定格式的正則,就能夠解決這個問題。segmentfault
對於這種格式,不難想到能夠用這個正則表達式來作判斷:/\[[0-9]+\]/gi,但是咱們還須要將匹配值取出來。這個時候查了下正則表達式的文檔(文檔點擊這裏),發現有一個match方法,能夠返回匹配成功的結果。那麼就讓咱們來作個測試:數組
const reg = /\[[0-9]+\]/gi; const str = "target[123123]"; const str1 = "target[]" if (reg.test(str)) { console.log('test success'); } if (!reg.test(str1)) { console.log('test fail'); } const matchResult = str.match(reg); console.log(matchResult); // ["[123123]"]
誒,咱們如今已經找到了解決這種問題的方法,那讓咱們趕忙來繼續改進下代碼。框架
function get(data, ...args) { const reg = /\[[0-9]+\]/gi; return args.map((item) => { const paths = item.split('.'); let res = data; paths.map((path) => { if (reg.test(path)) { const match = path.match(reg)[0]; // 將target[0]裏的target儲存到cmd裏 const cmd = path.replace(match, ''); // 獲取數組索引 const arrIndex = match.replace(/[\[\]]/gi, ''); res = res[cmd][arrIndex]; } else { res = res[path]; } }); return res; }); } const obj = { selector: { to: { toutiao: "FE Coder"} }, target: [1, 2, { name: 'byted'}]}; console.log(get(obj, 'selector.to.toutiao', 'target[0]', 'target[2].name'));
寫完趕忙運行一下,完美,輸出了正確的結果了。那麼到這裏就結束了?
但是總感受有點不妥,感受事情沒有那麼簡單。通常來講,面試題除了考驗你解決問題的能力以外,可能還考驗着你思考問題的全面性、嚴謹性。像上面那種寫法,若是用戶傳入了一個不存在的path鏈或者一些其餘特殊狀況,就可能致使整個程序crash掉。想下lodash.get調用方式,即便你傳入了錯誤的path,他也能夠幫你作處理,而且返回一個undefined。所以,咱們還須要完善這個方法。
function get(data, ...args) { const reg = /\[[0-9]+\]/gi; return args.map((item) => { const paths = item.split('.'); let res = data; paths.map(path => { try { if (reg.test(path)) { const match = path.match(reg)[0]; const cmd = path.replace(match, ''); const arrIndex = match.replace(/[\[\]]/gi, ''); res = res[cmd][arrIndex]; } else { res = res[path]; } } catch (err) { console.error(err); res = undefined; } }); return res; }); }
在這裏,咱們對每個path的處理進行了try catch處理。若出錯了,則返回undefined。哇,這樣看起來就比較穩了。
那麼,有沒有別的解決方法呢?
羣裏有一個大佬提出了一種更簡單也很取巧的解決方案,就是經過構建一個Function解決這個問題(Function的詳細介紹點擊這裏)。因爲代碼很簡單,我就直接貼出來了:
function get(data, ...args) { const res = JSON.stringify(data); return args.map((item) => (new Function(`try {return ${res}.${item} } catch(e) {}`))()); } const obj = { selector: { to: { toutiao: "FE Coder"} }, target: [1, 2, { name: 'byted'}]}; console.log(get(obj, 'selector.to.toutiao', 'target[0]', 'target[2].name', 'asd'));
看完以後,就兩個字,牛逼。
這種方法我認可一開始我確實沒想到,確實是很奇技淫巧。不過仔細思考了下,其實不少框架都用到了這個奇技淫巧。好比說vue裏,就使用new Function的方式來動態建立函數,解決執行動態生成的代碼的問題。
再好比說,Function.prototype.bind方法裏(我寫了個相似的bind方法:倉庫),也使用了Function來解決一些問題(fn.length丟失問題)。說明這個東西仍是挺有用的,得學習瞭解一波,說不定哪天就用到了。
更新
有人提到了那種Function的方式沒辦法處理如下的處理:
let obj = {time : new Date(), a : "this is a", b : 30};
由於JSON.stringfy後,Date、Function和RegExp類型的變量都會失效。對於這種狀況,評論區有個大佬(馮恆智)也提到了一種很好的解決方案:
function get(data, ...args) { return args.map((item) => (new Function('data',`try {return data.${item} } catch(e) {}`))(data)); }
除此以外, 代碼宇宙提出了另外一種解決方案,就是將"target[0]"分爲兩個key,也很簡單粗暴,就是將在split以前,將字符串裏的'['替換爲'.',將']'直接去掉。這樣就能夠將"target[0]"變爲"target.0"。具體代碼以下:
function get(data, ...args) { return args.map((item) => { let res = data; item .replace(/\[/g, ".") .replace(/\]/g, "") .split('.') .map(path => res = res && res[path]); return res; }) }
並且這兩種方式的好處在於,它也能夠處理多維數組的狀況。
學習完以後,最重要就是要總結,只有總結下來了,知識才是你本身的。那麼我來總結下文章想表達的內容:
本文地址在->本人博客地址, 歡迎給個 start 或 follow