一道面试题引发的思考

今天在认真干(划)活(水)的时候,看到群里有人发了一道头条的面试题,就顺便看了一下,发现挺有意思的,就决定分享给你们,而且给出个人解决方案和思考过程。javascript

题目以下:vue

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

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将每个参数分隔开,而后遍历取值,最终返回结果。git

function get(data, ...args) {
	return args.map((item) => {
		const paths = item.split('.');
		let res = data;
		paths.map(path => res = res[path]);
		return res;
	})
}
复制代码

一运行,果不其然,报错了。github

后来仔细看了一下提供的测试代码,发现竟然有target[0]这种东西。。竟然还带了个数组索引。面试

冷静分析一下,对于后面带了个索引的类型,好比'target[0]',咱们确定是要特殊对待的。因此,咱们首先得先识别到这种特殊的类型,而后再对它进行额外处理。正则表达式

这个时候,很快的就能够想到使用正则表达式来作这个事情。为何呢?由于像这种带有索引的类型,他们都有一个特点,就是有固定的格式:[num],那么咱们只须要能构造出能够匹配这种固定格式的正则,就能够解决这个问题。segmentfault

对于这种格式,不难想到能够用这个正则表达式来作判断:/\[[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的方式来动态建立函数,解决执行动态生成的代码的问题。

image-20181123104236389

image-20181123104312474

再好比说,Function.prototype.bind方法里(我写了个相似的bind方法:仓库),也使用了Function来解决一些问题(fn.length丢失问题)。说明这个东西仍是挺有用的,得学习了解一波,说不定哪天就用到了。

更新

有人提到了那种Function的方式没办法处理如下的处理:

let obj = {time : new Date(), a : "this is a", b : 30};
复制代码

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

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"。具体代码以下:

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. 有时候仍是能够多学习了解一下一些黑科技(好比Function),说不定哪天就能够用它来解决问题。

本文地址在->本人博客地址, 欢迎给个 start 或 follow

相关文章
相关标签/搜索