NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

5 道颇具挑战性的前端面试题

  • 2020-04-21
  • 本文字数:7461 字

    阅读完需:约 24 分钟

5道颇具挑战性的前端面试题


本文最初发布于 Medium 博客,经原作者授权由 InfoQ 中文站翻译并分享。


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


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

目录

  1. 模拟 Vue.js

  2. async series 和 parallel

  3. 能更改背景色的可拖动按钮

  4. 滑出动画

  5. Giphy 客户端

模拟 Vue.js

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


<div id="app">  {{ message }}</div>
复制代码


var app = new Vue({  el: '#app',  data: {    message: 'Hello Vue!'  }})
复制代码


你大概能猜得到这里的目标是用 Hello Vue!取代{{message}},当然不能将 Vue.js 添加成依赖项。


在开始研究代码之前,请务必与面试官交流,澄清你可能对问题抱有的任何疑问,并确保你完全理解输入、输出的内容,以及需要考虑的任何极端情况。


首先我们创建 Vue 类,并将其添加到 Javascript 代码段上方。


class Vue {    constructor(options) {    }}
复制代码


这样,我们的小项目至少应该能正确运行。


现在为了用提供​​的文本替换模板字符串,可能最简单的方法是,一旦我们可以访问 #app 元素,就在其 innerHTML 属性上使用 String.replace():


class Vue {  constructor(options) {    const el = document.querySelector(options.el);    const data = options.data;        Object.keys(data).forEach(key => {      el.innerHTML = el.innerHTML.replace(        `{{ ${key} }}`,        data[key]      );    });}
复制代码


这样工作就完成了,但是我们绝对可以做得更好。例如,如果我们有两个名称相同的模板字符串,那么这个实现就无法按预期正常运行。只有第一次出现的字符串才会被替换。


<div id="app">  {{ message }} and {{ message }}, what's the {{ message }}</div>
复制代码


这很容易解决,我们使用一个正则表达式(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 即可,但是一旦标记变得更加复杂就很快不够用了:


<div id="app">  {{ message }}  <p> another {{ message }} inside a paragraph </p></div>
复制代码


你会注意到< p>标签将从 DOM 中删除。这是因为 innerText 和 textContent 仅返回文本,当我们将其用作 setter 时,它会将标记替换为仅文本。


一种解决方法是遍历 DOM,找到所有文本节点,然后替换文本。


Vue {  constructor(options) {    this.el = document.querySelector(options.el);    this.data = options.data;    this.replaceTemplateStrings();  }  replaceTemplateStrings() {    const stack = [this.el];    while (stack.length) {      const n = 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 节点实际上如此简单:


<p>Nothing to see here</p>
复制代码


我们仍将迭代 200 次来查找模板字符串。


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


class Vue {  constructor(options) {    this.el = document.querySelector(options.el);    this.data = options.data;    this.replaceTemplateStrings();  }  replaceTemplateStrings() {    const stack = [this.el];    while (stack.length) {      const n = stack.pop();      if (n.childNodes.length) {        stack.push(...n.childNodes);      }      if (n.nodeType === Node.TEXT_NODE) {        this.replaceText(n);      }    }  }  replaceText(node) {    let text = node.textContent;    let result = "";    let state = 0; // 0 searching template, 1 searching key    let cursor = 0;    for (let i = 0; i < text.length - 1; i++) {      switch (state) {        case 0:          if (text[i] === "{" && text[i + 1] === "{") {            state = 1;            result += text.substring(cursor, i);            cursor = i;          }          break;        case 1:          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 callbackfunction(err, results) {    // results is now equal to ['one', 'two']});
复制代码


首先我们来创建一个异步对象:


const async = {    series: (tasks, callback) => {}};
复制代码


这项挑战的主要内容是,我们需要确保函数是一个个执行的,换句话说我们只在上一个函数完成后才执行下一个函数:


const async = {  series: (tasks, callback) => {    let i = 0;    const results = [];    const _callback = (err, result) => {      results[i] = result;      if (err || ++i >= tasks.length) {        callback(err, results);        return;      }      tasks[](_callback);    };    tasks[](_callback);  }};
复制代码


我们使用一个变量 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 callbackfunction(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) => {    let done = false;    let count = 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


首先我们来创建标记:


<html>  <body>    <div id="overlay"></div>    <div id="button" draggable="true"></div>  </body><html>
复制代码


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(https://rxjs-dev.firebaseapp.com/api/index/function/fromEvent)和 subscribe(https://rxjs-dev.firebaseapp.com/api/index/class/Observable#subscribe)来解决前者。这里全都可以使用标准 javascript 来完成(参见 addEventListener「https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener」)。


import { fromEvent } from "rxjs";import { distinctUntilChanged, filter } from "rxjs/operators";const button = document.querySelector("#button") as HTMLElement;const overlay = document.querySelector("#overlay") as HTMLElement;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 阻止所有重复事件。


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


const maxY = window.innerHeight / 2;const y = Math.abs(event.clientY - maxY);const pY = y / maxY;const maxX = window.innerWidth / 2;const x = Math.abs(event.clientX - maxX);const pX = 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:


<html>  <body>    <div id="box"></div></body><html>
复制代码


然后是 CSS:


#box {    width: 50px;    height: 50px;    background-color: blue;}
复制代码


使用 Java 脚本实现动画的方法不止一种。我建议使用 window.requestAnimationFrame(https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame):


const slideOut = (element, duration) => {  const initial = 0;  const target = window.innerWidth;  const start = new Date();  const loop = () => {    const time = (new Date().getTime() - start.getTime()) / 1000; // in seconds    const value = (time * target) / duration + initial;    box.style.transform = `translateX(${value}px)`;        if (value >= target) {      box.style.transform = ``;      return;    }    window.requestAnimationFrame(loop);  };  window.requestAnimationFrame(loop);};const box = document.getElementById("box");box.addEventListener("click", event => {  slideOut(event.target, 1);});
复制代码


我们添加了一个单击事件侦听器,以便每次单击 #box 时,都会使用元素和动画的持续时间来调用 slideOut。


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


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


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


easeInQuad = function (t, b, c, d) { return c*(t/=d)*t + b; };
复制代码


把第 9 行改为:


const value = target * (time / duration) * (time / duration) + initial;
复制代码


结果如下:



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


https://medium.com/better-programming/creating-a-proximity-graph-animation-an-introduction-to-html5-canvas-and-the-animation-loop-45719d82d1a3

Giphy 客户端

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


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


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


import React, { useState } from "react";export default function App() {  const [query, setQuery] = useState("");  return (    <div>      <h1>Giphy Client</h1>      <div>        <form>          <input value={query} onChange={e => setQuery(e.target.value)} />          <input type="submit" value="Search" />        </form>      </div>    </div>  );}
复制代码


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


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


const search = () => {  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 方法。


<form  onSubmit={e => {    e.preventDefault(); // prevents the page from reloading    search();  }}>
复制代码


最后,我们扩展组件以从搜索结果中渲染 GIF:


{data && (  <div>    <h2>Results</h2>    <ul>      {data.map(d => (        <li key={d.id}>          <img src={d.images.fixed_width.url} alt={d.id} />        </li>      ))}    </ul>  </div>)}
复制代码


再加上一些基本的 CSS 后,结果如下:



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

延伸阅读

https://medium.com/better-programming/5-front-end-interview-coding-challenges-6cd9f31d1169#8e35


2020-04-21 10:265118

评论

发布
暂无评论
发现更多内容
5道颇具挑战性的前端面试题_大前端_Vinicius De Antoni_InfoQ精选文章