具有挑战性的前端面试题

209 阅读8分钟

去年我面试了多家科技公司的软件工程师职位。由于其中多数都是 Web 开发岗位,因此我当然要回答许多客户端开发方面的问题。有些问题很简单,比如:什么是事件委托?如何在 Java 中实现继承?还有一些是更具挑战性的上手编程问题,而在本文中我就会分享其中我最喜欢的 5 道面试题。

毫无疑问,面试成功的关键是做好充分的准备。因此,无论你是在积极参加面试,抑或只是有些好奇,想知道科技公司面试前端岗位时可能会问什么样的问题,这篇文章都能帮得上你的忙,让你为将来的面试打下更好的基础。

目录

模拟 Vue.js async series 和 parallel 能更改背景色的可拖动按钮 滑出动画 Giphy 客户端 模拟 Vue.js

我在一次电话面试中遇到了这个挑战。对方让我转到 Vue.js 文档,并将以下代码段复制到我用的编辑器中:

 复制代码

<divid="app"> {{ 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] ); }); } 这样工作就完成了,但是我们绝对可以做得更好。例如,如果我们有两个名称相同的模板字符串,那么这个实现就无法按预期正常运行。只有第一次出现的字符串才会被替换。

 复制代码

<divid="app"> {{ message }}and{{ message }}, what's the{{ message }}

这很容易解决,我们使用一个正则表达式( developer.mozilla.org/en-US/docs/… ),带有全局标记 newRegExp( {{ {key}}} , “g”) 而不是 {{{key} }} 。

另外,innerHTML 开销很大,因为值会被解析为 HTML,所以我们应该使用 textContent 或 innerText。要进一步了解三者之间的区别,请看这里:

developer.mozilla.org/en-US/docs/…

对于我们的简单标记来说只需将 innerHTML 替换为 innerText 或 textContent 即可,但是一旦标记变得更加复杂就很快不够用了:

 复制代码

<divid="app"> {{ 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 次来查找模板字符串。

解决这个问题的一种方法是实现一个简单的状态机,这个状态机只查看一次文本,并随即替换模板字符串(如果存在):

 复制代码

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 知识),要是能进一步讨论如何实现循环和条件( vuejs.org/v2/guide/#C… )并处理用户输入( vuejs.org/v2/guide/#H… )就更好了。

你可以在下面的沙箱中看到上面代码的运行效果(译注:平台所限无法展示原文的沙箱,请点击文末的原文链接查看沙箱运行效果,后同):

async series 和 parallel

在 RxJ、Promises 和 async/await 成为行业标准之前,编写 Javascript 异步代码并不是一件容易的事情,而且你经常会掉进回调地狱( callbackhell.com/ )里面。正因如此,像 async 这样的库诞生了。

接下来的两部分是我在一次现场面试中遇到的挑战。他们让我带上自己的笔记本电脑,所以我知道面试中会有现场编程环节。

async.series

async.series( caolan.github.io/async/v3/do… )会依次运行 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( caolan.github.io/async/v3/do… )会并行运行函数的 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 可以跟踪已完成的函数数量,这样我们就能知道何时应该停止。我们有一个内部回调,负责收集结果并调用用户的回调。最后,我们会一次性触发所有函数。

最终代码效果如下:

用来更改背景颜色的可拖动按钮

在一次现场面试中,他们要求我在屏幕中间实现一个可拖动的按钮。当它移向边缘时,背景颜色从白色变为红色。

在讨论可能的解决方案之前,请在此处查看结果和代码:

codesandbox.io/s/drag-to-c…

首先我们来创建标记:

 复制代码

overlay 将覆盖整个屏幕,这是我们用来更改背景颜色的元素。#button 是我们的可拖动按钮。

下面是 CSS 代码,用来给按钮添加样式并加入 overlay:

 复制代码

#button{ cursor: pointer; background-color: black; width:50px; height:50px; border-radius:50px; position: absolute; top:50%; left:50%; transform:translateX(-50%)translateY(-50%); } #overlay{ background-color: red; width:100vw; height:100vh; z-index: -1; opacity:0; } 我们更改颜色的方法是调整覆盖层(overlay)的不透明度。默认值为 0(透明),我们将使用 javascript 来做相应的更改。

在这次挑战期间他们允许我使用任何库,因为我知道这家公司使用的是 Typescript 和 RxJS,所以我决定使用它们。我们需要做两件事:订阅和处理拖动事件,并根据事件 X 和 Y 的坐标确定覆盖层的不透明度。

我们将使用 fromEvent( rxjs-dev.firebaseapp.com/api/index/f… )和 subscribe( rxjs-dev.firebaseapp.com/api/index/c… )来解决前者。这里全都可以使用标准 javascript 来完成(参见 addEventListener「 developer.mozilla.org/en-US/docs/… 」)。

 复制代码

import{ fromEvent }from"rxjs"; import{ distinctUntilChanged, filter }from"rxjs/operators"; const button =document.querySelector("#button")asHTMLElement; const overlay =document.querySelector("#overlay")asHTMLElement; fromEvent(document,"drag") .pipe( filter((event: DragEvent)=>event.target === button), distinctUntilChanged((e1: DragEvent, e2: DragEvent)=> e1.clientX === e2.clientX && e1.clientY === e2.clientY) ) .subscribe((event: DragEvent)=>{ //calculate overlay opacity }); 我们 filter 掉所有目标不是#button 的拖动事件,并使用 distinctUntilChanged 阻止所有重复事件。

我们需要做一些数学运算才能解决后者。

 复制代码

constmaxY =window.innerHeight /2; consty =Math.abs(event.clientY - maxY); constpY = y / maxY; constmaxX =window.innerWidth /2; constx =Math.abs(event.clientX - maxX); constpX = x / maxX; overlay.style.opacity =String(Math.max(pY, pX)); event.clientY 和 event.clientX 表示可拖动按钮在屏幕上的位置。基于这些,我们需要计算一个介于 0 和 1 之间的数字,这将是覆盖层的不透明度。

我们将 x 和 y 的最大值分别设置为 window.innerHeight 和 window.innerWidth 除以 2。我们将 x 和 y 归一化为介于 0 和最大值之间的值。最后,我们计算 pY 和 pX(它们是介于 0 和 1 之间的值),并将不透明度设置为其中较高的那个值。

滑出动画

以我的经验,关于元素如何动画化的问题是很常见的。我参加的那次面试中,他们要求我做的事是为元素点击实现一个滑出动画,而不能使用 CSS 动画和过渡。

首先我们来做 HTML:

 复制代码

然后是 CSS:

 复制代码

#box{ width:50px; height:50px; background-color: blue; } 使用 Java 脚本实现动画的方法不止一种。我建议使用 window.requestAnimationFrame( developer.mozilla.org/en-US/docs/… ):

 复制代码

constslideOut =(element, duration) =>{ constinitial =0; consttarget =window.innerWidth; conststart =newDate(); constloop =()=>{ consttime = (newDate().getTime() - start.getTime()) /1000;// in seconds constvalue = (time * target) / duration + initial; box.style.transform =translateX(${value}px);

if(value >= target) { box.style.transform =``; return; } window.requestAnimationFrame(loop); }; window.requestAnimationFrame(loop); }; constbox =document.getElementById("box"); box.addEventListener("click",event=>{ slideOut(event.target,1); }); 我们添加了一个单击事件侦听器,以便每次单击#box 时,都会使用元素和动画的持续时间来调用 slideOut。

slideOut 函数定义了 transformX 转换的 initial 和 target。创建一个 loop 并使用 requestAnimationFrame 调用它。循环将一直执行到#box 到达屏幕底部为止。使用线性方程式计算每个新 value。

经常会问到的一个后续问题是,你将如何实现一个 easing 函数( easings.net/en# )?

还好我们已经有了将线性方程切换到某个 Penner 方程( robertpenner.com/easing/penn… )上所需的所有参数( blog.moagrius.com/actionscrip… )。这里就用 easeInQuad:

 复制代码

easeInQuad =function(t, b, c, d) {returnc*(t/=d)*t+ b; }; 把第 9 行改为:

 复制代码

constvalue= target * (time/duration) * (time/duration) + initial; 结果如下:

如果你对 Javascript 动画感兴趣,我写了一篇关于它的文章以供参考:

medium.com/better-prog…

Giphy 客户端

对于我们要解决的最后一个挑战,我的任务是实现一个小型 Web 应用程序,该程序能让用户搜索和浏览 gif,用的是 Giphy API( developers.giphy.com/docs/api#qu… )。

面试时我可以自由选择我喜欢的框架和库。在本文中我将使用 React 和 fetch( developer.mozilla.org/en-US/docs/… )。

我们首先创建一个简单的 React 组件,其表单将处理用户输入:

 复制代码

importReact, { useState } from"react"; exportdefaultfunctionApp() { const [query, setQuery] = useState(""); return(

Giphy Client

setQuery(e.target.value)}/>
); } 如果时间允许,你应该考虑创建子组件以使代码井井有条。在面试中你的时间一般是没那么充裕的。所以即使你没有时间去做这种事情,也一定要让面试官知道你打算如何改进代码。

现在,为了使用 Giphy API,我们需要生成一个 API Key( http://y1zfwiomdykwy80gtsxu4iedv165yeod/ )。有了它就可以向组件中添加一个函数,以从搜索端点( developers.giphy.com/docs/api/en… )中获取数据。

 复制代码

constsearch =()=>{ if(!query) { setData(undefined); return; } fetch( https://api.giphy.com/v1/gifs/search?q=${query}&api_key=<API_KEY> ) .then(response=>response.json()) .then(json=>{ setData(json.data); }); }; 简单起见,对于任何 API 异常都没有错误处理。

现在,当用户点击 Search 或单击 ENTER 时,我们需要使 < form> 调用 search 方法。

 复制代码

{ e.preventDefault();// prevents the page from reloading search(); }} > 最后,我们扩展组件以从搜索结果中渲染 GIF:

 复制代码

{data && (

Results

    {data.map(d => ( ))}
)} 再加上一些基本的 CSS 后,结果如下:

感谢你的阅读,希望你今天学到了一些新知识。

延伸阅读 www.jianshu.com/p/86bb74768…