一道面試題引起的思考

一道面試題引起的思考

今天在認真幹(劃)活(水)的時候,看到群裡有人發了一道頭條的面試題,就順便看了一下,發現挺有意思的,就決定分享給大家,並且給出我的解決方案和思考過程。

題目如下:

實現一個get函數,使得下面的調用可以輸出正確的結果

const obj = { selector: { to: {/>get(obj, 'selector.to.toutiao', 'target[0]', 'target[2].name');
// [ 'FE Coder', 1, 'byted']

乍眼一看,這不就是實現一個lodash.get方法嗎?看上去好像很簡單。所以我就開始寫了第一個版本。思想其實很簡單,遍歷傳進來的參數,使用split將每一個參數分隔開,然後遍歷取值,最終返回結果。

function get(data, ...args) {
return args.map((item) => {
const paths = item.split('.');
let res = data;
paths.map(path => res = res[path]);
return res;
})
}

一運行,果不其然,報錯了。

後來仔細看了一下提供的測試代碼,發現居然有target[0]這種東西。。居然還帶了個數組索引。

冷靜分析一下,對於後面帶了個索引的類型,比如'target[0]',我們肯定是要特殊對待的。所以,我們首先得先識別到這種特殊的類型,然後再對它進行額外處理。

這個時候,很快的就可以想到使用正則表達式來做這個事情。為什麼呢?因為像這種帶有索引的類型,他們都有一個特色,就是有固定的格式:[num],那麼我們只需要能構造出可以匹配這種固定格式的正則,就可以解決這個問題。

對於這種格式,不難想到可以用這個正則表達式來做判斷:/\[[0-9]+\]/gi,可是我們還需要將匹配值取出來。這個時候查了下正則表達式的文檔([文檔點擊這裡](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/match)),發現有一個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: {/>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的詳細介紹點擊[這裡](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/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: {/>console.log(get(obj, 'selector.to.toutiao', 'target[0]', 'target[2].name', 'asd'));

看完之後,就兩個字,牛逼。

![image](http://upload-images.jianshu.io/upload_images/14004175-1d2335e66c1913eb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

這種方法我承認一開始我確實沒想到,確實是很奇技淫巧。不過仔細思考了下,其實很多框架都用到了這個奇技淫巧。比如說vue裡,就使用new Function的方式來動態創建函數,解決執行動態生成的代碼的問題。

再比如說,Function.prototype.bind方法裡(我寫了個類似的bind方法:[倉庫](https://github.com/chenjigeng/something/blob/master/bind/bind.js)),也使用了Function來解決一些問題(fn.length丟失問題)。說明這個東西還是挺有用的,得學習瞭解一波,說不定哪天就用到了。

更新

有人提到了那種Function的方式沒辦法處理以下的處理:

let obj = {time : new Date(), a : "this is a", b : 30};

因為JSON.stringfy後,Date、Function和RegExp類型的變量都會失效。對於這種情況,評論區有個大佬([馮恆智](https://segmentfault.com/u/fenghengzhi/about))也提到了一種很好的解決方案:


function get(data, ...args) {
return args.map((item) => (new Function('data',`try {return data.${item} } catch(e) {}`))(data));
}

除此之外, [代碼宇宙](https://segmentfault.com/u/universe_of_code/about)提出了另一種解決方案,就是將"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;
})
}

而且這兩種方式的好處在於,它也可以處理多維數組的情況。

### 總結

學習完之後,最重要就是要總結,只有總結下來了,知識才是你自己的。那麼我來總結下文章想表達的內容:

1. 對於具有固定格式的字符串,可以考慮使用正則表達式來識別和匹配。

2. 實現一個功能的時候,不要只考慮正常情況,要多考慮一些非正常情況,比如輸入格式不對、用戶不按套路來或者因為一些奇奇怪怪的事情報錯。並且能對可預見的非正常情況做一個容錯處理。

3. 有時候還是可以多學習瞭解一下一些黑科技(比如Function),說不定哪天就可以用它來解決問題。

一道面試題引起的思考


分享到:


相關文章: