产品战略专家梁宁确认出席AICon北京站,分享AI时代下的商业逻辑与产品需求 了解详情
写点什么

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:266349

评论

发布
暂无评论
发现更多内容

火山引擎ByteHouse:如何优化ClickHouse物化视图能力?

字节跳动数据平台

数据库 大数据 云原生

剧情继续:马斯克曝出OpenAI前员工举报信,董事会与奥特曼谈判回归

Openlab_cosmoplat

如何获取item_search_guang API中与“爱逛街”相关的API接口?

技术冰糖葫芦

API 文档

Nginx配置Websocket

EquatorCoco

HTTP websocket 协议解析

MySQL大表设计

EquatorCoco

MySQL 数据库 数据库云

软件测试/测试开发丨人工智能时代软件测试的变化

测试人

人工智能 软件测试

埃森哲使用 Amazon CodeWhisperer 助力开发人员提高工作效率

亚马逊云科技 (Amazon Web Services)

Java Python 人工智能 S3 Amazon CodeWhisperer

开源之夏 2023 | Databend 社区项目总结与分享

Databend

CleanMyMac X for mac下载 优化清理软件

iMac小白

低代码:数字化转型趋势下的快速开发方式

互联网工科生

低代码 数字化

企业如何选择一款高效的ETL工具

RestCloud

ETL

小程序开发“巨坑”多,华为云这款轻量应用服务器轻松避坑

YG科技

IT打工人避雷针!华为云这款轻量应用服务器是网站开发“神器”

YG科技

多个云平台,撑着零售消费企业们向上爬坡中

ToB行业头条

OmniGraffle Pro for mac(思维导图软件)v7.22.4激活版

mac

苹果mac Windows软件 OmniGraffle Pro 图形设计工具

pdf增强插件Enfocus PitStop Pro 2020 for Mac下载

iMac小白

智能监控,高效观测 IT 系统瓶颈

观测云

IT 智能监控

如何在淘宝的item_search_seller API中获取店铺列表?

技术冰糖葫芦

API 文档

JD-GUI 反编译jar包

javaNice

Java

编程新手如何提高编程能力?

代码生成器研究

阿里云崩溃损失大?华为云耀云服务器L实例为企业保驾护航

YG科技

cad2024 mac版更新 最新AutoCAD 2024中文破解版下载

iMac小白

2023 IoTDB Summit 应用实例议题详解 | 报名到场即送卫衣!

Apache IoTDB

idea如何新建一个多模块的springCloud项目

javaNice

Java SpringCloud

HashMap HashTable ConcurrentMap 中key value是否可以为null

javaNice

Java

低代码PaaS开发平台

树上有只程序猿

低代码 PaaS 私有化部署

C++ LibCurl实现Web指纹识别

不在线第一只蜗牛

c++ 编程 web socket LibC

专访|OpenTiny 开源社区 常浩:完成比完美更重要

OpenTiny社区

开源 Vue 前端 富文本编辑器

AppLink结合金蝶云星空作订单信息同步流程

RestCloud

零代码 APPlink

编程到底难在哪里?

代码生成器研究

低代码究竟能干什么?

代码生成器研究

5道颇具挑战性的前端面试题_大前端_Vinicius De Antoni_InfoQ精选文章