【AICon】探索RAG 技术在实际应用中遇到的挑战及应对策略!AICon精华内容已上线73%>>> 了解详情
写点什么

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

评论

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

Spring之JDK动态代理与CGLIB动态代理

echoes

面渣逆袭:MySQL六十六问,两万字+五十图详解

三分恶

MySQL 8月月更

不需要服务器,教你仅用30行代码搞定实时健康码识别

华为云开发者联盟

云计算 疫情 文字识别 健康码

阿里出品Spring Security王者晋级笔记,并附上源码解析

冉然学Java

编程 spring security java; Spring Boot CLI 源码、

重新审视分布式系统:永远不会有完美的一致性方案……

JAVA活菩萨

Java 程序员 java程序员 java 编程

如何防止重复下单?

三分恶

8月月更

攻防视角下,初创企业安全实战经验分享

声网

网络安全

阿里大牛最新分享springboot实战派文档,开发之路必不可少

Java工程师

Java spring springboot

iOS逆向之某茅台App抓包

小陈

iOS逆向 i茅台

iOS逆向之某多多App抓包

小陈

拼多多 iOS逆向

学python,怎么能不学习scrapy呢!

华为云开发者联盟

Python 爬虫 开发 代码

Python 教程之输入输出(4)—— 用于竞争性编程的 Python 输入法

海拥(haiyong.site)

Python 8月月更

面了三十个人,说说真实感受

三分恶

面试 面试官 8月月更

博云入选Gartner中国云原生领域代表性厂商

BoCloud博云

云计算 容器 云原生 Gartner

《2022 年上半年全球独角兽企业发展研究报告》发布——DEMO WORLD世界创新峰会圆满落幕

创业邦

现代 ABAP 编程语言中的正则表达式

Jerry Wang

正则表达式 编程语言 SAP abap 8月月更

NFT盲盒挖矿系统dapp开发NFT链游搭建

薇電13242772558

dapp NFT

6000字深度总结!极狐GitLab 首次揭秘内部 DevOps 最佳实践

极狐GitLab

git DevOps gitlab 安全 CI/CD

华为应用市场“图章链接”功能上线 让APP分发突破机型壁垒

Geek_2d6073

【iOS逆向】某车之家sign签名分析

小陈

iOS逆向

【Android逆向】rpc调用某安App的X-App-Token签名函数

小陈

逆向技术

RabbitMQ(五)死信队列

JAVA活菩萨

Java 程序员 后端 java程序员 java编程

力拓信创生态,博睿数据多款产品获得东方通与达梦数据库产品兼容互认证明

博睿数据

可观测性 智能运维 博睿数据 东方通 达梦数据

SAP ABAP SteamPunk 蒸汽朋克的最新进展 - 嵌入式蒸汽朋克

Jerry Wang

云原生 Cloud 云平台 SAP 8月月更

深入聊聊Linux五种IO模型

C++后台开发

epoll 网络模型 I/O模型 C/C++后台开发 C/C++开发

5种kafka消费端性能优化方法

华为云开发者联盟

大数据 kafka 后端 MRS

精品!阿里P7爆款《K8s+Jenkins》技术笔记,高质量干货必收藏!

冉然学Java

jenkins java; 技术笔记 编程‘’ #k8s

Typora收费?搭建VS Code MarkDown写作环境

三分恶

工具 markdown 8月月更

2022版史上最全Java八股文,没有任何异议

Java工程师

Java 面试 八股文

开源一夏 | 文件和目录操作|多进程和多线程【python进阶篇】

恒山其若陋兮

开源 8月月更

移动平台助力推进智慧型科研院所信息化建设

WorkPlus

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