Graph + AI 中国峰会火热报名中,点击探索图分析更多可能! 了解详情
写点什么

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

2020 年 4 月 21 日

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 年 4 月 21 日 10:262415

评论 1 条评论

发布
用户头像
内容很好,机器翻译很烂!


2020 年 10 月 22 日 00:26
回复
没有更多了
发现更多内容

技术团队内部管理思考

6:00 am

技术管理

Service Mesh 从“趋势”走向“无聊”

阿里巴巴云原生

Java 运维 云原生 dubbo 中间件

【架构实战营】第二模块总结

围绕工作的务实学习

架构实战营

牛客网官推!3天访问量直接破20W的Java高频面试汇总笔记太香了!

程序员小毕

Java 程序员 架构 面试 分布式

「前端初学者、硬件爱好者、编程自学者」微信小程序开发很简单!

智能物联实验室

前端 硬件设计 硬件研发

推荐!看完全面掌握,最详细的 Docker 学习笔记总结(2021最新版)

民工哥

Docker 程序员 容器 DevOps 运维

分布式系统当中的CAP理论

五分钟学大数据

分布式 CAP理论 4月日更

2021最新发布:Java面试突击大全 带你摸熟20+互联网公司面试考点

比伯

Java 编程 架构 程序人生 计算机

腾讯司晓:区块链如何在数字世界中重塑所有权?

CECBC区块链专委会

跨省通办,海淀在全国率先推出“区块链+”服务新模式

CECBC区块链专委会

让孩子爱上阅读(一)

箭上有毒

读书笔记 4月日更

【得物技术】得物前端性能监控实践

得物技术

前端 体验 监控 用户体验 实践

混沌工程缓存实战系列一Redis

心远

缓存 混沌工程

架构实战业命题二学习总结

Vic

架构实战营

我把这个软件,推荐给了总监

yes

人生向前

shun123456789

lakin跟投社区APP开发|lakin跟投社区软件系统开发

开發I852946OIIO

系统开发

跨链技术如何破解区块链的可扩展性难题?

CECBC区块链专委会

区块链

【架构实战营】第二模块作业

围绕工作的务实学习

架构实战营

架构实战营模块二命题作业

Vic

架构实战营

上线半天下载量破100W!美团大佬的Java性能调优实战手册,超详细

Crud的程序员

Java 编程 架构

马丁量化策略系统搭建,量化交易软件开发

13823153121

浪潮云说丨打造网络安全“铜墙铁壁”

浪潮云

云计算

微服务中台技术解析之sso登录实践

小江

Java 架构设计 后端开发 SSO

如何避免团队里出现搭便车现象

石云升

团队建设 28天写作 职场经验 管理经验 4月日更

8421的数学含义

山@支

Flutter 学习笔记(一) Text 组件

U+2647

flutter 4月日更

用APICloud开发仿微信聊天App制作经验分享

APICloud

小程序云开发 前端 web开发 APP开发 APICloud

streamlit:算法工程师快速编写demo的利器

行者AI

算法

第二课作业

杰语

爬虫实例:爬取中国大学排名Top20

Bob

Python python 爬虫 4月日更

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