關於一道前端筆試題的思考

嫌疑犯X發表於2018-03-01

昨天看到一篇面試阿里的文章
面試分享:專科半年經驗面試阿里前端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,數字的值,複雜度上升了一個數量級。不禁感嘆自己和大神之間的差距以光年計 。

本文完

相關文章