5 道颇具挑战性的前端面试题
本文最初发布于 Medium 博客,经原作者授权由 InfoQ 中文站翻译并分享。
去年我面试了多家科技公司的软件工程师职位。由于其中多数都是 Web 开发岗位,因此我当然要回答许多客户端开发方面的问题。有些问题很简单,比如:什么是事件委托?如何在 Java 中实现继承?还有一些是更具挑战性的上手编程问题,而在本文中我就会分享其中我最喜欢的 5 道面试题。
毫无疑问,面试成功的关键是做好充分的准备。因此,无论你是在积极参加面试,抑或只是有些好奇,想知道科技公司面试前端岗位时可能会问什么样的问题,这篇文章都能帮得上你的忙,让你为将来的面试打下更好的基础。
目录
- 模拟 Vue.js
- async series 和 parallel
- 能更改背景色的可拖动按钮
- 滑出动画
- Giphy 客户端
模拟 Vue.js
我在一次电话面试中遇到了这个挑战。对方让我转到 Vue.js 文档,并将以下代码段复制到我用的编辑器中:
复制代码
{{ message }}
复制代码
varapp = newVue({ el: '#app', data: { message: 'HelloVue!' } })
你大概能猜得到这里的目标是用 Hello Vue! 取代{{message}},当然不能将 Vue.js 添加成依赖项。
在开始研究代码之前,请务必与面试官交流,澄清你可能对问题抱有的任何疑问,并确保你完全理解输入、输出的内容,以及需要考虑的任何极端情况。
首先我们创建 Vue 类,并将其添加到 Javascript 代码段上方。
复制代码
classVue{ constructor(options) { } }
这样,我们的小项目至少应该能正确运行。
现在为了用提供的文本替换模板字符串,可能最简单的方法是,一旦我们可以访问#app 元素,就在其 innerHTML 属性上使用 String.replace():
复制代码
classVue{ constructor(options) { constel =document.querySelector(options.el); constdata = options.data; Object.keys(data).forEach(key=>{ el.innerHTML = el.innerHTML.replace( `{{${key}}}`, data[key] ); }); }
这样工作就完成了,但是我们绝对可以做得更好。例如,如果我们有两个名称相同的模板字符串,那么这个实现就无法按预期正常运行。只有第一次出现的字符串才会被替换。
复制代码
{{ message }}and{{ message }}, what's the{{ message }}
这很容易解决,我们使用一个正则表达式( https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp ),带有全局标记 newRegExp( {{ ${key}}}
, “g”) 而不是 {{ ${key} }}
。
另外,innerHTML 开销很大,因为值会被解析为 HTML,所以我们应该使用 textContent 或 innerText。要进一步了解三者之间的区别,请看这里:
https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent#Differences_from_innerText
对于我们的简单标记来说只需将 innerHTML 替换为 innerText 或 textContent 即可,但是一旦标记变得更加复杂就很快不够用了:
复制代码
{{ message }} another{{ message }}inside a paragraph
你会注意到 < p> 标签将从 DOM 中删除。这是因为 innerText 和 textContent 仅返回文本,当我们将其用作 setter 时,它会将标记替换为仅文本。
一种解决方法是遍历 DOM,找到所有文本节点,然后替换文本。
复制代码
Vue { constructor(options) { this.el = document.querySelector(options.el); this.data= options.data; this.replaceTemplateStrings(); } replaceTemplateStrings() { conststack = [this.el]; while(stack.length) { constn = stack.pop(); if(n.childNodes.length) { stack.push(...n.childNodes); } if(n.nodeType === Node.TEXT_NODE) { Object.keys(this.data).forEach(key => { n.textContent = n.textContent.replace( new RegExp(`{{ ${key} }}`,"g"), this.data[key] ); }); } } } }
还有一件事情也需要我们改进。每次我们要找到一个文本节点时,我们都会查找模板字符串 n 次(在本例中 n 是数据条目的数量)。因此,如果我们有 200 个条目,即便我们的 DOM 节点实际上如此简单:
复制代码
Nothing to see here
我们仍将迭代 200 次来查找模板字符串。
解决这个问题的一种方法是实现一个简单的状态机,这个状态机只查看一次文本,并随即替换模板字符串(如果存在):
复制代码
<
pre>classVue{
constructor(options) {
this.el = document.querySelector(options.el);
this.data = options.data;
this.replaceTemplateStrings();
}
replaceTemplateStrings() {
conststack= [this.el];
while(stack.length) {
constn =stack.pop();
if(n.childNodes.length) {
stack.push(…n.childNodes);
}
if(n.nodeType === Node.TEXT_NODE) {
this.replaceText(n);
}
}
}
replaceText(node) {
lettext= node.textContent;
let result =””;
let state =0;// 0 searching template, 1 searching key
letcursor=0;
for(let i =0; i <text.length -1; i++) {
switch(state) {
case0:
if(text[i] ===”{“&&text[i +1] ===”{“) {
state =1;
result +=text.substring(cursor, i);
cursor= i;
}
break;
case1:
if(text[i] ===”}”&&text[i +1] ===”}”) {
state =0;
result +=this.data[text.substring(cursor+2, i -1).trim()];
cursor= i +2;
}
break;
default:
}
}
result +=text.substring(cursor);
node.textContent = result;
到这一步离生产就绪还差不少,但你应该能在大约 30-45 分钟的时间内完成。
一定要说说你下一步的改进方向,谈谈性能问题(顺便炫耀一把你的 VirtualDOM 知识),要是能进一步讨论如何实现循环和条件( https://vuejs.org/v2/guide/#Conditionals-and-Loops )并处理用户输入( https://vuejs.org/v2/guide/#Handling-User-Input )就更好了。
你可以在下面的沙箱中看到上面代码的运行效果(译注:平台所限无法展示原文的沙箱,请点击文末的原文链接查看沙箱运行效果,后同):
async series 和 parallel
在 RxJ、Promises 和 async/await 成为行业标准之前,编写 Javascript 异步代码并不是一件容易的事情,而且你经常会掉进回调地狱( http://callbackhell.com/ )里面。正因如此,像 async 这样的库诞生了。
接下来的两部分是我在一次现场面试中遇到的挑战。他们让我带上自己的笔记本电脑,所以我知道面试中会有现场编程环节。
async.series
async.series( http://caolan.github.io/async/v3/docs.html#series )会依次运行 task 集合中的函数,每一个函数运行完毕后开始运行下一个。如果序列中的任何函数向其回调传递了一个错误,则不会再运行任何函数,并且会立即使用这个错误的值调用 callback。否则,当 task 完成时,callback 将收到一个结果数组。
复制代码
async.series([ function(callback){ // do some stuff ... callback(null,'one'); }, function(callback){ // do some more stuff ... callback(null,'two'); } ], // optional callback function(err, results){ // results is now equal to ['one', 'two'] });
首先我们来创建一个异步对象:
复制代码
constasync= { series:(tasks, callback) =>{} };
这项挑战的主要内容是,我们需要确保函数是一个个执行的,换句话说我们只在上一个函数完成后才执行下一个函数:
复制代码
constasync= { series:(tasks, callback) =>{ leti =0; constresults = []; const_callback =(err, result) =>{ results[i] = result; if(err || ++i >= tasks.length) { callback(err, results); return; } tasksi; }; tasks0; } };
我们使用一个变量 i 来跟踪正在执行的当前函数,并创建一个内部回调以检查错误、递增 i 并执行下一个函数。
简单起见,我们不会验证输入或使用 try/catch 来改善错误处理,但你应该同面试官谈到这些做法。
async.parallel
async.parallel( http://caolan.github.io/async/v3/docs.html#parallel )会并行运行函数的 task 集合,而无需等待上一个函数完成。如果任何一个函数将一个错误传递给它的回调,则立即使用这个错误的值调用主 callback。tasks 完成后,结果将作为一个数组传递到最终的 callback。
复制代码
async.parallel([ function(callback){ setTimeout(function(){ callback(null,'one'); },200); }, function(callback){ setTimeout(function(){ callback(null,'two'); },100); } ], // optional callback function(err, results){ // the results array will equal ['one','two'] even though // the second function had a shorter timeout. });
首先,向我们的异步对象添加一个新的并行函数:
复制代码
const async = { series:(tasks, callback)=>{} parallel:(tasks, callback)=>{} };
parallel 与 series 有所不同,在某种意义上说我们可以同时触发所有函数,我们只需小心收集结果,将它们放置在数组的正确位置上。
复制代码
parallel:(tasks,callback) =>{ letdone=false; letcount =0; const results =[]; const _callback =(i,err,result) =>{ count++; results[i]= result; if(!done&&(err||count===tasks.length)) { callback(err, results); done=true; return; } }; tasks.forEach((task,i)=> { task((err, result) =>_callback(i,err,result)); }); } };
我们从 done 标志开始,该标志可以防止在发生错误后调用回调,另外 count 可以跟踪已完成的函数数量,这样我们就能知道何时应该停止。我们有一个内部回调,负责收集结果并调用用户的回调。最后,我们会一次性触发所有函数。
最终代码效果如下:
用来更改背景颜色的可拖动按钮
在一次现场面试中,他们要求我在屏幕中间实现一个可拖动的按钮。当它移向边缘时,背景颜色从白色变为红色。
在讨论可能的解决方案之前,请在此处查看结果和代码:
https://codesandbox.io/s/drag-to-change-background-color-57dvw
首先我们来创建标记:
复制代码