昨天看到一篇面試阿里的文章
面試分享:專科半年經驗面試阿里前端P6+總結(附面試真題及答案)
對其中一道筆試題產生了興趣,特地把思考過程付諸於文字。
1.實現destructuringArray方法,
達到如下效果
// destructuringArray( [1,[2,4],3], "[a,[b],c]" );
// result
// { a:1, b:2, c:3 }
複製程式碼
原文作者給出了一個解法,實現方式比較巧妙,而且在臨場發揮的場景下,更加顯得難得,感興趣的可以去原文看一下。
現在拿到這個題,有充裕時間的去考慮。
先上測試用例
[1,[2,4],3], "[a,[b],c]" ==> {a:1,b:2,c:3}
[1,[2,3,[4]]], "[react,[vue,rxjs,[angular]]]" ==> {react:1,vue:2,rxjs:3,angular:4}
[1,[2,4],3], "[a,[[b],c]" ==> Error: too many left square brackets
[1,[2,4],3], "[a,[b]],c]" ==> Error: too many right square brackets ==>pos:9
[1,[2,4],3], "[33a,[b],c]" ==> Error: key is invalid varible => 33a
[1,[2,4],3], "[re[a]ct,[b],c]" ==> Error: invalid string==>pos:3
[1,[2,4],3], "rx,[react,[b],c]" ==> Error: template with invalid start or end
[1,[2,4],3], "[react,[b],c],rx" ==> Error: template with invalid start or end
複製程式碼
本文的目標就是要跑通以上的case.
我在探索這道題解法的過程中,首先思考面試官考察的是什麼。不及格的是不會做或者給了一個錯誤的解法;及格的是對於給定的用例得到正確結果;再好一點能對入參做基本的校驗;比較優秀的是能考慮絕大多數邊界情況,正確的用例返回正確結果,錯誤的用例丟擲相應異常。
繼而想到在做專案過程中,無論是寫業務邏輯,抑或封裝公共基礎庫,不但需要能處理正常流程,更重要的是能處理可能遇到的異常。這樣做出來的東西才會有更少的bug,畢竟bug是測試工程師走進一家酒吧要了-1杯啤酒引起的(笑)
然後筆者又發散思維,想到書寫程式碼過程或者在使用模板(如傳統的後端模板引擎或是前端jsx)的過程中,如果用了不合法的書寫方式,總能得到具體到位置的錯誤資訊。感嘆寫優秀的庫和框架需要思考一萬種測試工程師走進酒吧的方式,才能變得流行。
扯遠了,回到這個題。我們想用比較優秀的方式去解決這個題,就需要能考慮到這道題可能遇到的異常情況,並給出具體的錯誤資訊。
下面分3步來處理:
1、解析模板字串(例如"[a,[b],c]"),判斷是否是合法的陣列字串,如果不是的話給出具體的錯誤資訊,如果是的話返回解析後的結果。
2、根據第一步拿到的解析結果,跟陣列進行比對,如果結構不一致(比如[1,2] 對應 "[a,[b]]", b是在陣列的陣列裡,而2是個數字,沒辦法得到b),丟擲相應的異常。如果一致進入下一步。
3、根據陣列和第一步得到的解析結果輸出結果。
第一步 解析陣列字串
下面上程式碼,先看第一步的程式碼,這一步是最複雜的,需要處理陣列字串各種異常情況,如果字串合法需要拿到陣列的解析結果。
思路是遍歷陣列字串,獲取對應的 '[' 在的位置,以及陣列字串裡對應的key的資訊。以"[react,[vue,rxjs,[angular]]]"為例。輸出的資訊包括兩個, 一個是 陣列字串裡陣列的位置資訊陣列 為[[],[1],[1,2]](簡單來說,示例裡一共有三對方括號,根據方括號的深度得到每對方括號的位置,比如'[angular]'這個方括號的位置就是在第一層的第一個位置,再查詢第二個位置,記錄為[1,2]。再深的層次類推),另一個是key的位置資訊,返回一個keyMap,這裡
keyMap = {
react:[0],
vue:[1,0],
rxjs:[1,1],
angular:[1,2,0]
}
複製程式碼
得到key的位置資訊之後我們就可以使用真實arr 直接獲取key的值了,比如 angular的可以這樣獲取:arr[1][2][0]。
所以現在的關鍵和難點在於如何獲取這些資訊。這裡筆者用的思路是這樣的:
1、
遍歷陣列字串,
2、
只處理4種情況,左方括號([) 右方括號(]) 逗號(,) 和其他字元。
3、
定義一個指標index=0和一個key=""兩個區域性變數(index用來記錄當前位置,與遍歷時for迴圈裡的i不一樣,這裡index是用逗號分隔的每個變數的位置),
4、
(1)如果是普通字元char,key+=char,用來拼寫key
(2)如果是逗號(,) 說明變數key已經拼接完成,位置index也要加1,把key push進keyList 裡存起來
(3)如果是左方括號([),說明要步入一個新的陣列,
此時把當前的位置序號index push進indexList裡存起來,並把index置為0,因為新陣列的序號又從0開始了。
再把 indexList push進arrayPosList,代表當前新陣列在的位置。
(4)如果是右方括號(]),說明要步出當前陣列, 此時有兩步操作,1 是把 indexList裡最後一個元素(步入當前陣列時的位置)pop出來,賦值給index。 2 是key拼接完成,push進keyList(這裡還要多一步,頂部宣告一個keyMap={}物件,儲存key的位置資訊,keyMap[key] = [...indexList, index];indexList是當前陣列的位置資訊,加上index是key在當前陣列的位置,合一起就是key在整個陣列字串裡的位置資訊。括號裡這一步也應該加到第(2)小步)
舉個例子
在"[react,[vue,rxjs,[angular]]]"中,當遇到第一個字元‘[’時,indexList.push(index),indexList=[0]; 當遇到‘,’時,index加了1,再遇到第二個‘[’時,indexList.push(index),indexList=[0,1],把index置為0,再往後查詢到angular在的陣列方括號時,此時index通過加加操作變為了2,呼叫indexList.push(index),此時indexList 變為[0,1,2]。這樣陣列字串裡3個方括號的位置資訊就都有了
function parse(template) {
let indexList = [],
arrayPosList = [],
data = {},
keyList = [],
key = '',
index = 0,
keyMap = {},
spaceReg = /\s/g;
//去除字串裡的空格
template = template.replace(spaceReg, "");
let len = template.length;
for (let i = 0; i < len; i++) {
let char = template[i];
if (char == '[') {
indexList.push(index);
index = 0;
arrayPosList.push(indexList.slice(1)) //indexList裡邊第0個元素0,是第一個方括號的位置, 是多餘的,這裡從第1個開始
} else if (char == ']') {
if (key !== '') {
keyList.push(key);
keyMap[key] = [...indexList.slice(1), index]; //indexList裡邊第0個元素0,是第一個方括號的位置, 是多餘的,這裡從第1個開始
key = '';
}
index = indexList.pop();
} else if (char == ',') {
if (key !== '') {
keyList.push(key);
keyMap[key] = [...indexList.slice(1), index]; //indexList裡邊第0個元素0,是第一個方括號的位置, 是多餘的,這裡從第1個開始
key = '';
}
index++;
} else {
key += char;
}
prevChar = char;
}
console.log("arrayPosList==>",arrayPosList,"\nkeyMap==>", keyMap)
return [arrayPosList, keyMap];
}
複製程式碼
到這裡正常的陣列字串已經能解析了,我們試著呼叫一下parse("[react,[vue,rxjs,[angular]]]"),輸出如下
arrayPosList==> [ [], [ 1 ], [ 1, 2 ] ]
keyMap==> { react: [ 0 ],
vue: [ 1, 0 ],
rxjs: [ 1, 1 ],
angular: [ 1, 2, 0 ] }
複製程式碼
已經能正確的解析出陣列的結構資訊和每個變數對應的位置了。
處理異常
但是,假如使用者的入參不符合標準的話,如 "[react,[vue]"(多了左方括號)、"[react,vue]]"(多了右方括號)、"[react,v[u]e]"(括號位置錯亂)、"[33react,@vue]]"(變數不合法)等諸多異常情況都沒有處理,下面我們給出處理異常情況的程式碼,註釋裡有解釋是處理什麼樣的異常
function parse(template) {
let indexList = [],
arrayPosList = [],
data = {},
keyList = [],
key = '',
index = 0,
keyMap = {},
spaceReg = /\s/g;
*********//去除空格 這一步已經丟失了字元最初的位置,還需要進行完善 *********
template = template.replace(spaceReg, "");
let len = template.length;
*********//處理異常case "js,[react,vue]" 或 "[react,vue],js"*********
if (template[0] !== '[' || template[len - 1] !== ']') {
throw new Error('template with invalid start or end')
}
for (let i = 0; i < len; i++) {
let char = template[i];
if (char == '[') {
let prevChar = template[i - 1];
*********//左方括號前邊必須是‘[’、‘,’或者undefined,undefined對應陣列字串第一個左方括號前邊的位置。 例如 "[r[eact],vue]"*********
if (prevChar !== undefined && prevChar !== ',' && prevChar != '[') {
throw new Error('invalid string==>pos:'+i)
}
indexList.push(index);
index = 0;
arrayPosList.push(indexList.slice(1)) //indexList裡邊第0個元素0,是第一個方括號的位置, 是多餘的,這裡從第1個開始
} else if (char == ']') {
let nextChar = template[i + 1];
*********//右方括號後邊必須是‘]’、‘,’或者undefined,undefined對應陣列字串最後一個右方括號後邊的位置。 例如 "[react,[vu]e]"*********
if (nextChar !== undefined && nextChar !== ',' && nextChar != ']') {
throw new Error('invalid string==>pos:'+i)
}
if (key !== '') {
keyList.push(key);
keyMap[key] = [...indexList.slice(1), index]; //indexList裡邊第0個元素0,是第一個方括號的位置, 是多餘的,這裡從第1個開始
key = '';
}
index = indexList.pop();
*********//如果index是undefined,說明indexList空了,說明右方括號比左方括號多,結構不對*********
if (index === undefined) {
throw new Error('too many right square brackets ==>pos:' + i)
}
} else if (char == ',') {
if (key !== '') {
keyList.push(key);
keyMap[key] = [...indexList.slice(1), index]; //indexList裡邊第0個元素0,是第一個方括號的位置, 是多餘的,這裡從第1個開始
key = '';
}
index++;
} else {
key += char;
}
}
*********//如果indexList 還有元素,說明左方括號比右方括號多*********
if (indexList.length > 0) {
throw new Error('too many left square brackets')
}
*********//檢查js變數合法性的正則*********
let reg = /^(_|\$|[A-Za-z])(_|\$|\w)*$/
keyList.forEach(key => {
if (!reg.test(key)) {
throw new Error('key is invalid varible => ' + key)
}
})
console.log("arrayPosList==>",arrayPosList,"\nkeyMap==>", keyMap)
return [arrayPosList, keyMap];
}
複製程式碼
處理異常的邏輯比較簡單,難點在於是否能考慮到所有的異常case並進行相應處理。
第二步 對比陣列和陣列字串的結構
接下來的事情就比較簡單了,拿到陣列字串的結構和所有key的位置資訊,我們就可以很輕鬆的拿到key對應真實陣列的value了。
接下來先把真實陣列arr和陣列字串的結構進行比對,邏輯比較簡單,下面直接給出程式碼
// arr=[1,[2,3,[4]]] template = "[react,[vue,rxjs,[angular]]]" ==parse(template)==> arrayPosList = [ [], [ 1 ], [ 1, 2 ] ]
function check(arr, arrayPosList) {
遍歷arrayPosList,對陣列字串中每個陣列的位置資訊,檢視arr裡對應的位置是否是陣列,如果有一個檢查不通過,說明arr與陣列字串的結構對應不上。比如這裡的arr=[1,[2,3,4]], 4和[angular] 對應,這時angular無法解析了,這裡會丟擲異常
arrayPosList.forEach(item => {
if (item.length == 0) return;
let ret = arr;
try {
for (let i = 0; i < item.length; i++) {
ret = ret[item[i]]
}
} catch (e) {
throw new Error('invalid structure');
return;
}
if (!Array.isArray(ret)) {
throw new Error('invalid structure');
}
})
}
複製程式碼
第三步 解析獲得資料
最後一步,就是根據keyMap從陣列裡取得對應的value了。經過前面的parse和check步驟,到了這一步說明陣列和陣列字串是能夠進行解析的。
function destructuringArray(arr, template) {
//必要的校驗
if (!Array.isArray(arr)) {
throw new Error('invalid first argument');
}
//必要的校驗
if (typeof template !== 'string') {
throw new Error('invalid second argument');
}
//最終返回結果
let ret = {};
//解析陣列字串
let [arrayPosList, keyMap] = parse(template);
//檢查陣列和字串的結構是否match
check(arr, arrayPosList);
//遍歷keyMap,取到每一個key對應的value
Object.keys(keyMap).forEach(key => {
let path = keyMap[key];
let val = arr;
for (let i = 0; i < path.length; i++) {
val = val[path[i]]
}
ret[key] = val
})
//輸出
console.log(ret, '========')
return ret;
}
複製程式碼
做完這道題,筆者又發散了一下思維,假如字串需要解析‘{’和‘}’, 又能處理 null,undefined,true,false,數字的值,複雜度上升了一個數量級。不禁感嘆自己和大神之間的差距以光年計 。
本文完