【AICon】AI 大模型超全落地场景&最佳实践 了解详情
写点什么

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

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

    阅读完需:约 24 分钟

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

AI 大模型超全落地场景&金融应用实践,8 月 16 - 19 日 FCon x AICon 大会联诀来袭、干货翻倍!


本文最初发布于 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:265941

评论

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

聊聊产品中的状态机设计

产品海豚湾

产品经理 产品设计 产品开发 需求分析 主业务流程梳理

2022年的各大平台小游戏生态发展到哪一步了?

FN0

游戏开发 小游戏开发 小程序游戏开发

当打造一款极速湖分析产品时,我们在想些什么

StarRocks

数据湖 湖仓一体

专科非科班怎么选择培训机构

小谷哥

明道云联合契约锁共建人事场景电子签约解决方案

明道云

Linux RT 进程引发内核频繁卡死的优化方案

火山引擎边缘云

云计算 Linux 云原生 边缘计算 火山引擎边缘计算

学习大数据该怎么选择培训机构?

小谷哥

零基础学web前端,哪些培训机构比较好

小谷哥

5分钟带你彻底掌握async底层实现原理!

千锋IT教育

极光笔记 | 以静制动:行为触发营销助力用户转化

极光JIGUANG

营销 运营 消息推送 用户运营

通过WSL2运行GUI程序

DisonTangor

WSL2 GUI

AH协议

穿过生命散发芬芳

12月月更 AH协议

2022-12-15:寻找用户推荐人。写一个查询语句,返回一个客户列表,列表中客户的推荐人的编号都 不是 2。 对于示例数据,结果为: +------+ | name | +------+ | Wil

福大大架构师每日一题

数据库· 福大大

极客时间运维进阶训练营第八周作业

9527

北京哪家web前端开发机构比较好?

小谷哥

java软件培训班毕业后找工作吗

小谷哥

KCL 与其他 Kubernetes 配置管理工具的异同 - Kustomize 篇 [一个自研编程语言能做什么?(系列 2)]

Peefy

开发者 工具 编程语言 Kubernetes Serverless #DevOps

明道云携手衡石科技共建企业应用数据分析联合解决方案

明道云

2022全球边缘计算大会,火山引擎荣获“优质边缘云服务提供商”称号

火山引擎边缘云

云原生 CDN 边缘计算 边云协同 火山引擎边缘计算

React源码分析(二)渲染机制

flyzz177

React

瓴羊Quick BI 填报组件让数据分析和可视化呈现轻而易举

对不起该用户已成仙‖

可视化:数据可视化发展史

Data 探险实验室

数据分析 可视化 数据可视化

低代码实现探索(五十四)低代码的描述文本

零道云-混合式低代码平台

React源码分析1-jsx转换及React.createElement

flyzz177

React

第五届“强网”拟态防御国际精英挑战赛在南京举行

科技热闻

openEuler 倡议建立 eBPF 软件发布标准

openEuler

开源 云原生 操作系统 内核 ebpf

chatGPT的爆火,是计算机行业这次真的“饱和”了?

千锋IT教育

从vivo的创新方法论中,读懂高端突破的“因果”

脑极体

【从零开始学爬虫】采集收视率排行数据

前嗅大数据

大数据 数据采集 爬虫软件 爬虫教程 数据采集教程

架构实战 - 模块3作业

mm

学生管理系统架构 #架构实战营

零代码平台在政府智慧城市领域的应用

明道云

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