昨天看到一篇面试阿里的文章
面试分享:专科半年经验面试阿里前端P6+总结(附面试真题及答案)
对其中一道笔试题产生了兴趣,特意把思考过程付诸于文字。前端
1.实现destructuringArray方法,
达到以下效果
// destructuringArray( [1,[2,4],3], "[a,[b],c]" );
// result
// { a:1, b:2, c:3 }
复制代码
原文做者给出了一个解法,实现方式比较巧妙,并且在临场发挥的场景下,更加显得可贵,感兴趣的能够去原文看一下。vue
如今拿到这个题,有充裕时间的去考虑。react
先上测试用例面试
[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杯啤酒引发的(笑)bash
而后笔者又发散思惟,想到书写代码过程或者在使用模板(如传统的后端模板引擎或是前端jsx)的过程当中,若是用了不合法的书写方式,总能获得具体到位置的错误信息。感叹写优秀的库和框架须要思考一万种测试工程师走进酒吧的方式,才能变得流行。框架
扯远了,回到这个题。咱们想用比较优秀的方式去解决这个题,就须要能考虑到这道题可能遇到的异常状况,并给出具体的错误信息。post
下面分3步来处理:测试
一、解析模板字符串(例如"[a,[b],c]"),判断是不是合法的数组字符串,若是不是的话给出具体的错误信息,若是是的话返回解析后的结果。
二、根据第一步拿到的解析结果,跟数组进行比对,若是结构不一致(好比[1,2] 对应 "[a,[b]]", b是在数组的数组里,而2是个数字,没办法获得b),抛出相应的异常。若是一致进入下一步。
三、根据数组和第一步获得的解析结果输出结果。
下面上代码,先看第一步的代码,这一步是最复杂的,须要处理数组字符串各类异常状况,若是字符串合法须要拿到数组的解析结果。
思路是遍历数组字符串,获取对应的 '[' 在的位置,以及数组字符串里对应的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]。
因此如今的关键和难点在于如何获取这些信息。这里笔者用的思路是这样的:
遍历数组字符串,
只处理4种状况,左方括号([) 右方括号(]) 逗号(,) 和其余字符。
定义一个指针index=0和一个key=""两个局部变量(index用来记录当前位置,与遍历时for循环里的i不同,这里index是用逗号分隔的每一个变量的位置),
(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,数字的值,复杂度上升了一个数量级。不由感叹本身和大神之间的差距以光年计 。
本文完