今天在认真干 (划) 活(水)的时候, 看到群里有人发了一道头条的面试题, 就顺便看了一下, 发现挺有意思的, 就决定分享给大家, 并且给出我的解决方案和思考过程.
题目如下:
实现一个 get 函数, 使得下面的调用可以输出正确的结果
- 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 将每一个参数分隔开, 然后遍历取值, 最终返回结果.
- 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, 可是我们还需要将匹配值取出来. 这个时候查了下正则表达式的文档(文档点击这里), 发现有一个 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: { 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. 因此, 我们还需要完善这个方法.
- 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 的详细介绍点击这里). 由于代码很简单, 我就直接贴出来了:
- 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 的方式没办法处理以下的处理:
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;
- })
- }
而且这两种方式的好处在于, 它也可以处理多维数组的情况.
总结
学习完之后, 最重要就是要总结, 只有总结下来了, 知识才是你自己的. 那么我来总结下文章想表达的内容:
对于具有固定格式的字符串, 可以考虑使用正则表达式来识别和匹配.
实现一个功能的时候, 不要只考虑正常情况, 要多考虑一些非正常情况, 比如输入格式不对, 用户不按套路来或者因为一些奇奇怪怪的事情报错. 并且能对可预见的非正常情况做一个容错处理.
有时候还是可以多学习了解一下一些黑科技(比如 Function), 说不定哪天就可以用它来解决问题.
来源: https://juejin.im/post/5bf769e0518825773a2ebfe5