«
前端面试总结

时间:2023-3   


目录:

初级开发者相关问题【共计 1 道题】

31.JS 中继承方式有哪些?【JavaScript】

1、借助构造函数实现继承

call和apply改变的是JS运行的上下文:

/*借助构造函数实现继承*/
function Parent(name) {
    this.name = name;
    this.getName = function () {
        console.log(this.name);
    }
}

function Child(name) {
    Parent.call(this, name);
    this.type = 'child1'
}

let child = new Child('yanle');
child.getName();
console.log(child.type);

父类的this指向到了子类上面去,改变了实例化的this 指向,导致了父类执行的属性和方法,都会挂在到 子类实例上去;
缺点:父类原型链上的东西并没有被继承;

2、通过原型链实现继承

/*通过原型链实现继承*/
function Parent2(){
    this.name='parent2'
}

function Child2(){
    this.type='child2'
}

Child2.prototype=new Parent2();
console.log(new Child2());

Child2.prototype是Child2构造函数的一个属性,这个时候prototype被赋值了parent2的一个实例,实例化了新的对象Child2()的时候, 会有一个proto属性,这个属性就等于起构造函数的原型对象,但是原型对象被赋值为了parent2的一个实例, 所以new Child2的原型链就会一直向上找parent2的原型

var s1=new Child2();
var s2=new Child2();
s1.proto===s2.proto;//返回true

缺点:通过子类构造函数实例化了两个对象,当一个实例对象改变其构造函数的属性的时候, 那么另外一个实例对象上的属性也会跟着改变(期望的是两个对象是隔离的赛);原因是构造函数的原型对象是公用的;

3、组合方式

/*组合方式*/
function Parent3(){
    this.name='parent3';
    this.arr=[1,2,3];
}

function Child3(){
    Parent3.call(this);
    this.type='child';
}

Child3.prototype=new Parent3();
var s3=new Child3();
var s4=new Child3();
s3.arr.push(4);
console.log(s3,s4);

优点:这是最通用的使用方法,集合了上面构造函数继承,原型链继承两种的优点。
缺点:父类的构造函数执行了2次,这是没有必要的,
constructor指向了parent了

4、组合继承的优化

/*组合继承的优化1*/
function Parent4(){
    this.name='parent3';
    this.arr=[1,2,3];
}

function Child4(){
    Parent4.call(this);
    this.type='child5';
}

Child4.prototype=Parent4.prototype;
var s5=new Child4();
var s6=new Child4()

缺点:s5 instaceof child4 //true, s5 instanceof Parent4//true
我们无法区分一个实例对象是由其构造函数实例化,还是又其构造函数的父类实例化的
s5.constructor 指向的是Parent4;//原因是子类原型对象的constructor 被赋值为了父类原型对象的 constructor,所以我们使用constructor的时候,肯定是指向父类的
Child3.constructor 也有这种情况

5、组合继承的优化2

function Parent5() {
    this.name = 'parent5';
    this.play = [1, 2, 3];
}

function Child5() {
    Parent5.call(this);
    this.type = 'child5'
}

Child5.prototype = Object.create(Parent5.prototype);
//这个时候虽然隔离了,但是constructor还是只想的Parent5的,因为constructor会一直向上找
Child5.prototype.constructor=Child5;

var s7=new Child5();
console.log(s7 instanceof Child5,s7 instanceof Parent5);
console.log(s7.constructor);

通过Object.create来创建原型中间对象,那么这么来的话,chiild5的对象prototype获得的是parent5 父类的原型对象;
Object.create创建的对象,原型对象就是参数;

6、ES 中的继承

Class 可以通过extends关键字实现继承,让子类继承父类的属性和方法。extends 的写法比 ES5 的原型链继承,要清晰和方便很多。

class Point { /* ... */ }

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

中级开发者相关问题【共计 3 道题】

226.前端动画有哪些实现方式?【JavaScript】【出题公司: 阿里巴巴】

主要的实现方式

JS 的实现方式

CSS 3

HTML 5

requestAnimationFrame

window.requestAnimationFrame()  告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。 该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

当你准备更新动画时你应该调用此方法。这将使浏览器在下一次重绘之前调用你传入给该方法的动画函数 (即你的回调函数)。回调函数执行次数通常是每秒 60 次,但在大多数遵循 W3C 建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。

回调函数会被传入 DOMHighResTimeStamp 参数,DOMHighResTimeStamp指示当前被  requestAnimationFrame()  排序的回调函数被触发的时间。在同一个帧中的多个回调函数,它们每一个都会接受到一个相同的时间戳,即使在计算上一个回调函数的工作负载期间已经消耗了一些时间。该时间戳是一个十进制数,单位毫秒,最小精度为 1ms(1000μs)。

使用语法:  window.requestAnimationFrame(callback);

参数: 下一次重绘之前更新动画帧所调用的函数 (即上面所说的回调函数)。该回调函数会被传入  DOMHighResTimeStamp  参数,该参数与  performance.now()  的返回值相同,它表示  requestAnimationFrame()  开始去执行回调函数的时刻。

使用示范:

<div id="demo"
     style="position: absolute;width: 100px;height: 100px;background-color: #ccc;left: 0;top: 0;">
</div>
<script>
  var demo = document.getElementById("demo");
  function reader() {
    demo.style.left = parseInt(demo.style.left) + 1 + "px";// 每一帧向右移动1px
  }
  requestAnimationFrame(function() {
    reader();

    // 当超过300px 后才停止
    if (parseInt(demo.style.left) > 1300) demo.style.left = 0;
    requestAnimationFrame(arguments.callee);
  });
</script>

transition

属性名 说明
transition 用于简写设置四个过渡属性,包括:transition-property, transition-duration, transition-timing-function 和 transition-delay
transition-property 规定应用过渡效果的 CSS 属性的名称,多个属性用逗号分隔,如:transition-property: width, height;,表示在 width 和 height 发生改变时会应用过渡效果
transition-duration 规定过渡效果的持续时间,如:transition-duration: 1s;,表示过渡效果持续时间为 1 秒
transition-timing-function 规定过渡效果的时间曲线,即过渡效果的速度变化。常用的值包括:linear, ease, ease-in, ease-out, ease-in-out, cubic-bezier(n,n,n,n)
transition-delay 规定过渡效果何时开始,如:transition-delay: 1s;,表示在 1 秒之后开始应用过渡效果

可以通过设置不同的 transition 属性,来实现各种各样的过渡效果。例如:

cssCopy codediv {
  width: 100px;
  height: 100px;
  background-color: red;
  transition: width 1s ease-in-out;
}

div:hover {
  width: 200px;
}

上述代码表示当鼠标悬停在 div 元素上时,它的宽度会从 100px 变为 200px,过渡效果会持续 1 秒,且速度曲线为缓进缓出。

animation

下面是 CSS3 animation 的属性表格:

属性 描述
animation-name 规定需要绑定到选择器的 keyframe 名称。
animation-duration 规定动画完成一个周期所花费的秒或毫秒。
animation-timing-function 规定动画的速度曲线。
animation-delay 规定在动画开始之前的延迟。
animation-iteration-count 规定动画应该播放的次数。
animation-direction 规定是否应该轮流反向播放动画。
animation-fill-mode 规定当动画不播放时(当动画完成之前,或当动画被暂停时),要应用到元素的样式。
animation-play-state 规定动画是否正在运行或已暂停。
animation 是 animation 属性的一个简写属性,包含六个独立属性。

其中,animation-name、animation-duration、animation-timing-function 是必须要指定的属性。其他属性都是可选的。通过这些属性,我们可以实现更加灵活的动画效果。

一个使用  animation  的例子是实现旋转动画。例如,可以创建一个带有 CSS 类名  .rotate  的  div  元素,并使用以下样式:

cssCopy code.rotate {
  animation: spin 2s linear infinite;
}

@keyframes spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

这将在  div  元素上应用一个旋转动画,持续时间为 2 秒,并且以线性方式无限循环。在  @keyframes  规则中定义了旋转动画的动画过程。在  from  和  to  关键帧中,定义了元素旋转的起始和结束状态。在  animation  属性中,指定了动画名称、持续时间、时间函数和动画播放次数等参数。

Canvas 实现动画

Canvas 可以通过一帧帧的绘制来实现动画。具体来说,可以通过  requestAnimationFrame  方法在浏览器下一次重绘之前执行指定的回调函数来不断地更新 Canvas 上的内容,从而实现动画效果。

以下是 Canvas 实现动画的一般流程:

  1. 获取 Canvas 对象和上下文对象

首先,需要获取 Canvas 对象和上下文对象。

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
  1. 设置动画帧数和初始状态

为了实现动画,需要对 Canvas 进行重绘。重绘的次数由动画的帧数决定,通常设置为每秒 60 帧。

同时,还需要设置 Canvas 的初始状态,包括背景颜色、形状、大小等。

  1. 定义动画函数

动画函数中主要包含两个部分:更新状态和绘制图形。更新状态指更新 Canvas 上的图形的位置、大小、颜色等属性,绘制图形指将更新后的图形绘制到 Canvas 上。

function animate() {
  // 更新状态
  // ...

  // 绘制图形
  // ...
}
  1. 使用 requestAnimationFrame 方法执行动画

最后,可以使用  requestAnimationFrame  方法不断执行动画函数,从而实现动画效果。

function animate() {
  // 更新状态
  // ...

  // 绘制图形
  // ...

  // 递归调用 requestAnimationFrame 方法执行动画
  requestAnimationFrame(animate);
}

// 启动动画
requestAnimationFrame(animate);

在动画函数中更新状态和绘制图形后,调用  requestAnimationFrame  方法递归执行动画函数,从而实现不断更新和绘制的动画效果。

svg 实现动画

SVG(可缩放矢量图形)是一种使用 XML 描述 2D 图形的格式,它可以使用 CSS 和 JavaScript 进行动画操作。在 SVG 中,可以使用两种技术实现动画,分别是 SMIL(Synchronized Multimedia Integration Language)和 JavaScript。

下面举一个使用 JavaScript 实现 SVG 动画的例子。假设有一个圆形,当鼠标悬停在圆形上时,圆形会变为红色并且向右移动:

SVG 代码:

<svg width="200" height="200">
  <circle id="circle" cx="50" cy="50" r="20" fill="blue" />
</svg>

CSS 代码:

#circle {
  transition: fill 0.3s ease;
}

JavaScript 代码:

var circle = document.getElementById('circle');

circle.addEventListener('mouseover', function() {
  circle.setAttribute('fill', 'red');
  circle.setAttribute('cx', '70');
});

上面的代码中,通过给圆形添加 mouseover 事件监听器,当鼠标悬停在圆形上时,修改圆形的 fill 属性为红色,并将圆心的 x 坐标改为 70。由于圆形在 CSS 中定义了过渡效果,因此圆形会平滑地变为红色并向右移动。

227.进程、线程、协程分别是什么概念?【JavaScript】【出题公司: 小米】

进程(Process)和 线程(Thread)

进程(Process)和 线程(Thread)是操作系统中的重要概念。

进程是指计算机中已经运行的程序,它是操作系统资源分配的最小单位。进程拥有独立的内存空间和系统资源,如打开的文件、网络连接等。在操作系统中,每个进程都拥有一个唯一的标识符,称为进程ID。

线程是进程中的执行单元,一个进程可以包含多个线程,它们共享进程的内存空间和系统资源。线程是CPU调度的最小单位,它可以看作是进程中的一个独立执行流程。与进程不同的是,线程没有自己的系统资源,只有一部分与进程共享的资源。在操作系统中,每个线程都拥有一个唯一的标识符,称为线程ID。

可以将进程和线程的关系类比为一家工厂。工厂代表一个进程,工厂中的工人代表线程。每个工人负责自己的一部分工作,但是他们共享工厂的资源,如原材料、设备等。

总的来说,进程和线程都是操作系统资源分配和调度的基本单位,它们之间的关系是多对一的,即多个线程可以属于同一个进程,共享进程的资源。

协程(Coroutine)

协程(Coroutine)是一种用户态的轻量级线程,也称为协作式多任务处理,与传统的抢占式多任务处理方式不同,协程的调度不由系统来控制,而是由程序员自己控制。在协程内部,程序可以自己决定在何处挂起、何时恢复执行。协程可以有效地避免多线程并发操作时出现的死锁、竞争、状态同步等问题,同时协程又可以充分利用 CPU 资源,提高程序执行效率。

在协程中,所有任务共享一个线程,通过在任务之间切换来实现并发,这种方式可以避免线程切换时的性能损耗,也可以避免线程之间的同步问题。协程主要有以下特点:

协程在很多语言中都得到了广泛的应用,例如 Python 中的 asyncio、Lua 中的 coroutine 等。在前端领域中,JavaScript 的 Generator 函数就是一种协程实现方式。

228.单线程的 nodejs 是如何充分利用计算机 CPU 资源的呢?【Nodejs】

虽然 Node.js 是单线程的,但是它能够充分利用计算机的 CPU 资源的原因在于其采用了事件驱动和异步 I/O 的方式来处理请求,而不是采用阻塞式 I/O 的方式。这使得 Node.js 能够在处理一个请求时不会因为等待 I/O 操作而阻塞,从而可以处理更多的请求。

具体来说,当 Node.js 启动一个程序时,会创建一个事件循环,不断地从事件队列中取出一个事件,然后调用相应的回调函数来处理该事件。当有新的请求到来时,Node.js 会将其添加到事件队列中,等待事件循环处理。同时,Node.js 还采用了非阻塞式 I/O 的方式,即在等待 I/O 操作时不会阻塞其他代码的执行,从而能够更好地利用 CPU 资源。

此外,Node.js 还采用了基于事件的回调机制来处理异步请求,这种机制可以避免线程切换和上下文切换带来的开销,提高 CPU 利用率。因此,虽然 Node.js 是单线程的,但是它能够充分利用计算机 CPU 资源,处理更多的请求。

高级开发者相关问题【共计 4 道题】

141.如何组织 monorepo 工程?【工程化】

参考文档:

235.[webpack] 什么情况下 webpack treeShaking 会失效?【工程化】【出题公司: 小米】

以下是一些可能导致 webpack tree shaking 失效的情况

  1. 代码中使用了动态引入(Dynamic Imports)的语法,这种情况下,webpack 无法确定哪些代码会被使用,因此不会进行 tree shaking。
  2. 代码使用了函数式编程的方式,比如使用了 map、filter、reduce 等高阶函数,而这些函数很难通过静态分析确定代码的执行路径,所以可能会导致 tree shaking 失效。
  3. 代码中使用了 webpack 无法识别的模块系统,比如使用了 AMD 或者 CommonJS 的语法,这种情况下 webpack 也无法进行 tree shaking。
  4. 代码使用了 side effect,比如改变全局变量或者函数的参数,这种情况下 webpack 也无法进行 tree shaking。

函数式编程的方式 filter 为何会导致无法 tree shaking

函数式编程中常常使用高阶函数来组合函数,这种组合方式常常需要使用传递函数作为参数的方式,例如 map、filter 等高阶函数。这种情况下,如果参数传递的是一个函数表达式或者函数声明,那么无法进行 treeshaking。

举个例子:

// 代码中定义了一个 sum 函数
function sum(a, b) {
  return a + b;
}

// 调用了 lodash 库的 filter 函数,传递一个匿名函数表达式作为参数
import { filter } from 'lodash';

const arr = [1, 2, 3, 4, 5];
const result = filter(arr, item => {
  if (item > 10) return sum(item, 1)
  else return item;
});

上述代码中,使用了 lodash 库的 filter 函数,并且传递了一个匿名函数表达式作为参数。由于函数表达式无法被静态分析,不知道 sum 是否会被调用,因此无法进行 treeshaking,最终导致整个 sum 函数也被打包进了最终的代码中。

为什么 commonjs 模块化会导致无法 tree shaking

CommonJS 模块化语法是 Node.js 中的模块化规范,其使用了  require()  导入模块,使用  module.exports  或  exports  导出模块。它采用的是动态导入(require())和同步加载的方式,这种导入方式无法在编译时确定所依赖的模块,因此在 Webpack 进行 Tree Shaking 时,这种导入方式的模块会被认为无法被静态分析,因而会被排除掉。

相反,ES6 模块化语法采用的是静态导入的方式,例如  import foo from './foo.js' ,可以在编译时分析出所依赖的模块,因此支持 Tree Shaking。

因此,如果要使用 Tree Shaking,建议采用 ES6 模块化语法。如果必须使用 CommonJS 模块化规范,可以尝试使用动态导入 (import()) 语法,或者采用其他工具或手动实现 Tree Shaking。

side effect 是什么,为何会导致无法 tree shaking

在编写 JavaScript 代码时,如果一个函数除了返回值外,还对外部的变量产生了其他的影响,比如修改了全局变量、读写了文件等操作,那么这个函数就被称为有“副作用”(side effect)。因为这种函数并不是纯函数,它可能会影响其他部分的代码执行结果,不便于优化和调试。

在 Tree Shaking 的过程中,webpack 将模块打包成单独的 JavaScript 文件,它会从模块中找出哪些代码没有被使用到,并删除这些代码。但是,如果模块中存在带有副作用的代码,这些代码虽然没有被使用到,但它们仍然会被保留下来,因为这些代码可能会对其他部分的代码产生影响,因此不能简单地删除。这也是为什么带有副作用的代码会导致无法 Tree Shaking 的原因。

236.babel 的工作流程是如何的?【工程化】

Babel 是一个 JavaScript 编译器,它的主要功能是将新版本的 JavaScript 代码转换成向后兼容的代码。Babel 的工作流程可以简单概括为以下几个步骤:

  1. 解析:将 JavaScript 代码解析成 AST(抽象语法树)。
  2. 转换:对 AST 进行遍历,进行代码转换。
  3. 生成:将转换后的 AST 生成 JavaScript 代码。

具体来说,Babel 的工作流程如下:

  1. Babel 使用 babylon 解析器将 JavaScript 代码解析成 AST,babylon 是一个基于 AST 的 JavaScript 解析器。
  2. Babel 使用 babel-traverse 遍历器对 AST 进行遍历,找到需要转换的节点,进行转换。
  3. Babel 使用 babel-core 转换器将 AST 转换成 JavaScript 代码。babel-core 是 babel 的核心模块,它包含了所有的转换器和插件。
  4. Babel 使用 babel-generator 生成器将转换后的 AST 生成 JavaScript 代码。babel-generator 是一个将 AST 转换成 JavaScript 代码的工具。

在整个流程中,Babel 还会使用 babel-preset-env、babel-plugin-transform-runtime、babel-polyfill 等插件和工具来完成更加复杂的任务,如将 ES6 模块转换成 CommonJS 模块,使用 Polyfill 来实现一些新的 API 等。

需要注意的是,Babel 的转换过程是有损的,转换后的代码不一定与原始代码完全相同,也可能存在性能问题。因此,在使用 Babel 进行转换时,需要谨慎选择转换的规则和插件,以确保转换后的代码正确、高效。

237.canvas 与 svg 在可视化领域优劣如何【web应用场景】【出题公司: 腾讯】

Canvas和SVG都可以用于可视化,但它们的优缺点不同。

Canvas: 是一个基于像素的渲染引擎,使用JavaScript API在画布上绘制图像,它的优点包括:

但它也存在一些缺点:

SVG: 是一种基于矢量的图形格式,可以使用XML和JavaScript API在浏览器中绘制图像,它的优点包括:

但它也存在一些缺点:

表格对比

特性 Canvas SVG
图形质量 像素级别的图形,适合绘制大量复杂动态的图形 矢量图,图形不会失真,适合绘制静态图形
图形渲染 快速渲染,适合处理大量图形数据 慢速渲染,适合处理小规模静态图形
交互性 事件处理复杂,需要手动编写交互逻辑 事件处理简单,内置事件处理机制
动画效果 动画效果需要手动实现,实现复杂动画困难 内置 SMIL 动画支持,可实现较复杂动画效果
浏览器支持 除 IE8 及以下版本外,其他浏览器都支持 除 IE9 及以下版本外,其他浏览器都支持
适用场景 处理大量动态图形,如游戏开发、数据可视化等 绘制简单静态图形,如图标、线条、文字等

资深开发者相关问题【共计 2 道题】

231.不用使用 vue-cli ,如何创建一个完整的 vue 工程?【工程化】

这个一个较为复杂和庞大的话题, 不能称之为问题, 只能说它是一个话题。

主要涉及到的话题如下:

  1. vue 工程初始化
  2. 测试集成
  3. UI 库绑定、基础组件使用
  4. 开发流程
  5. 代码规范(甚至包含 commit 规范)
  6. 多人协作与工作流
  7. 构建问题
  8. 上线流程
  9. 线上日志与用户反馈问题排查
  10. 性能保证

232.使用同一个链接, 如何实现 PC 打开是 web 应用、手机打开是一个 H5 应用?【web应用场景】【出题公司: 小米】

可以通过根据请求来源(User-Agent)来判断访问设备的类型,然后在服务器端进行适配。例如,可以在服务器端使用 Node.js 的 Express 框架,在路由中对不同的 User-Agent 进行判断,返回不同的页面或数据。具体实现可以参考以下步骤:

  1. 根据 User-Agent 判断访问设备的类型,例如判断是否为移动设备。可以使用第三方库如 ua-parser-js 进行 User-Agent 的解析。
  2. 如果是移动设备,可以返回一个 H5 页面或接口数据。
  3. 如果是 PC 设备,可以返回一个 web 应用页面或接口数据。

具体实现方式还取决于应用的具体场景和需求,以上只是一个大致的思路。