JS面试之数组对象解构

数组解构:

将 destructuringArray([1, [2, 3], 4], “[a, [b], c]”) => {a: 1, b: 2, c: 4}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 将 destructuringArray([1, [2, 3], 4], "[a, [b], c]") => {a: 1, b: 2, c: 4}
const targetArray = [1, [2, 3], 4];
const formater = "[a, [b], c]";

const destructuringArray = (values, keys) => {
try {
const obj = {};
if (typeof keys === 'string') {
keys = JSON.parse(keys.replace(/\w+/g, '"$&"'));
}

const iterate = (values, keys) =>
keys.forEach((key, i) => {
if(Array.isArray(key)) iterate(values[i], key)
else obj[key] = values[i]
})

iterate(values, keys)

return obj;
} catch (e) {
console.error(e.message);
}
}

对象解构

实现一个get函数,使得下面的调用可以输出正确的结果

1
2
3
4
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将每一个参数分隔开,然后遍历取值,最终返回结果。

1
2
3
4
5
6
7
8
function get(data, ...args) {
return args.map((item) => {
const paths = item.split('.');
let res = data;
paths.map(path => res = res[path]);
return res;
})
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const obj = {
selector: {
to: {
toutiao: "FE Coder"
}
},
target: [1, 2, {
name: 'byted'
}]
};


function get(obj, ...list) {
return list.map((item) => {
return item.split(".").reduce((a = {}, b) => {
if(/\[([0-9])\]/g.test(b)){
var c=/(\w+)\[([0-9])\]/g.exec(b)
return a[c[1]]&&a[c[1]][c[2]]
}
return a[b]
}, obj)
})
}
get(obj, 'selector.to.toutiao', 'target[0]', 'target[2].name','zhangsan.lisi[3].name')

console.log(get(obj, 'selector.to.toutiao', 'target[0]', 'target[2].name','zhangsan.lisi[3].name'));

一运行,果不其然,报错了。
后来仔细看了一下提供的测试代码,发现居然有target[0]这种东西。。居然还带了个数组索引。
冷静分析一下,对于后面带了个索引的类型,比如’target[0]’,我们肯定是要特殊对待的。所以,我们首先得先识别到这种特殊的类型,然后再对它进行额外处理。

这个时候,很快的就可以想到使用正则表达式来做这个事情。为什么呢?因为像这种带有索引的类型,他们都有一个特色,就是有固定的格式:[num],那么我们只需要能构造出可以匹配这种固定格式的正则,就可以解决这个问题。

对于这种格式,不难想到可以用这个正则表达式来做判断:/[[0-9]+]/gi,可是我们还需要将匹配值取出来。这个时候查了下正则表达式的文档(文档点击这里),发现有一个match方法,可以返回匹配成功的结果。那么就让我们来做个测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
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]"]

诶,我们现在已经找到了解决这种问题的方法,那让我们赶紧来继续改进下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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。因此,我们还需要完善这个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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的详细介绍点击这里)。由于代码很简单,我就直接贴出来了:

1
2
3
4
5
6
7
8
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的方式没办法处理以下的处理:

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

因为JSON.stringfy后,Date、Function和RegExp类型的变量都会失效。对于这种情况,评论区有个大佬(冯恒智)也提到了一种很好的解决方案:

1
2
3
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”。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
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
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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'));


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


function get2(data, ...args) {
return args.map(item => {
const reg = /\[([0-9])\]/g;
if (reg.test(item)) {
item = item.replace(reg, '.$1');
}
const paths = item.split('.');
return paths.reduce((result, current) => {
return result[current];
}, data);
});
}

const get3 = (obj, ...args) => args.map(key => eval(`obj.${key}`));



function get4(obj, ...list) { //自己实现
return list.map((item) => {
return item.split(".").reduce((a = {}, b) => {
if(/\[([0-9])\]/g.test(b)){
var c=/(\w+)\[([0-9])\]/g.exec(b)
return a[c[1]]&&a[c[1]][c[2]]
}
return a[b]
}, obj)
})
}

get4(obj, 'selector.to.toutiao', 'target[0]', 'target[2].name','zhangsan.lisi[3].name')

  1. 对于具有固定格式的字符串,可以考虑使用正则表达式来识别和匹配。
  2. 实现一个功能的时候,不要只考虑正常情况,要多考虑一些非正常情况,比如输入格式不对、用户不按套路来或者因为一些奇奇怪怪的事情报错。并且能对可预见的非正常情况做一个容错处理。
  3. 有时候还是可以多学习了解一下一些黑科技(比如Function),说不定哪天就可以用它来解决问题。

一道面试题引起的思考