2023.03.28 - 2023.03.29 更新前端面试问题总结(10道题)TXAPP.TV
获取更多面试问题可以访问
github 地址: https://github.com/pro-collection/interview-question/issues
gitee 地址: https://gitee.com/yanleweb/interview-question/issues
目录:
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 指向,导致了父类执行的属性和方法,都会挂在到 子类实例上去;
缺点:父类原型链上的东西并没有被继承;
/*通过原型链实现继承*/
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
缺点:通过子类构造函数实例化了两个对象,当一个实例对象改变其构造函数的属性的时候, 那么另外一个实例对象上的属性也会跟着改变(期望的是两个对象是隔离的赛);原因是构造函数的原型对象是公用的;
/*组合方式*/
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了
/*组合继承的优化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 也有这种情况
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创建的对象,原型对象就是参数;
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()
}
}
JS 的实现方式
CSS 3
HTML 5
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-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 秒,且速度曲线为缓进缓出。
下面是 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 可以通过一帧帧的绘制来实现动画。具体来说,可以通过 requestAnimationFrame 方法在浏览器下一次重绘之前执行指定的回调函数来不断地更新 Canvas 上的内容,从而实现动画效果。
以下是 Canvas 实现动画的一般流程:
首先,需要获取 Canvas 对象和上下文对象。
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
为了实现动画,需要对 Canvas 进行重绘。重绘的次数由动画的帧数决定,通常设置为每秒 60 帧。
同时,还需要设置 Canvas 的初始状态,包括背景颜色、形状、大小等。
动画函数中主要包含两个部分:更新状态和绘制图形。更新状态指更新 Canvas 上的图形的位置、大小、颜色等属性,绘制图形指将更新后的图形绘制到 Canvas 上。
function animate() {
// 更新状态
// ...
// 绘制图形
// ...
}
最后,可以使用 requestAnimationFrame 方法不断执行动画函数,从而实现动画效果。
function animate() {
// 更新状态
// ...
// 绘制图形
// ...
// 递归调用 requestAnimationFrame 方法执行动画
requestAnimationFrame(animate);
}

// 启动动画
requestAnimationFrame(animate);
在动画函数中更新状态和绘制图形后,调用 requestAnimationFrame 方法递归执行动画函数,从而实现不断更新和绘制的动画效果。
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 中定义了过渡效果,因此圆形会平滑地变为红色并向右移动。
进程(Process)和 线程(Thread)是操作系统中的重要概念。
进程是指计算机中已经运行的程序,它是操作系统资源分配的最小单位。进程拥有独立的内存空间和系统资源,如打开的文件、网络连接等。在操作系统中,每个进程都拥有一个唯一的标识符,称为进程ID。
线程是进程中的执行单元,一个进程可以包含多个线程,它们共享进程的内存空间和系统资源。线程是CPU调度的最小单位,它可以看作是进程中的一个独立执行流程。与进程不同的是,线程没有自己的系统资源,只有一部分与进程共享的资源。在操作系统中,每个线程都拥有一个唯一的标识符,称为线程ID。
可以将进程和线程的关系类比为一家工厂。工厂代表一个进程,工厂中的工人代表线程。每个工人负责自己的一部分工作,但是他们共享工厂的资源,如原材料、设备等。
总的来说,进程和线程都是操作系统资源分配和调度的基本单位,它们之间的关系是多对一的,即多个线程可以属于同一个进程,共享进程的资源。
协程(Coroutine)是一种用户态的轻量级线程,也称为协作式多任务处理,与传统的抢占式多任务处理方式不同,协程的调度不由系统来控制,而是由程序员自己控制。在协程内部,程序可以自己决定在何处挂起、何时恢复执行。协程可以有效地避免多线程并发操作时出现的死锁、竞争、状态同步等问题,同时协程又可以充分利用 CPU 资源,提高程序执行效率。
在协程中,所有任务共享一个线程,通过在任务之间切换来实现并发,这种方式可以避免线程切换时的性能损耗,也可以避免线程之间的同步问题。协程主要有以下特点:
协程在很多语言中都得到了广泛的应用,例如 Python 中的 asyncio、Lua 中的 coroutine 等。在前端领域中,JavaScript 的 Generator 函数就是一种协程实现方式。
虽然 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 资源,处理更多的请求。
参考文档:
函数式编程中常常使用高阶函数来组合函数,这种组合方式常常需要使用传递函数作为参数的方式,例如 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 模块化语法是 Node.js 中的模块化规范,其使用了 require() 导入模块,使用 module.exports 或 exports 导出模块。它采用的是动态导入(require())和同步加载的方式,这种导入方式无法在编译时确定所依赖的模块,因此在 Webpack 进行 Tree Shaking 时,这种导入方式的模块会被认为无法被静态分析,因而会被排除掉。
相反,ES6 模块化语法采用的是静态导入的方式,例如 import foo from 'http://www.toutiao.com/a7215996421307187712/foo.js',可以在编译时分析出所依赖的模块,因此支持 Tree Shaking。
因此,如果要使用 Tree Shaking,建议采用 ES6 模块化语法。如果必须使用 CommonJS 模块化规范,可以尝试使用动态导入(import())语法,或者采用其他工具或手动实现 Tree Shaking。
在编写 JavaScript 代码时,如果一个函数除了返回值外,还对外部的变量产生了其他的影响,比如修改了全局变量、读写了文件等操作,那么这个函数就被称为有“副作用”(side effect)。因为这种函数并不是纯函数,它可能会影响其他部分的代码执行结果,不便于优化和调试。
在 Tree Shaking 的过程中,webpack 将模块打包成单独的 JavaScript 文件,它会从模块中找出哪些代码没有被使用到,并删除这些代码。但是,如果模块中存在带有副作用的代码,这些代码虽然没有被使用到,但它们仍然会被保留下来,因为这些代码可能会对其他部分的代码产生影响,因此不能简单地删除。这也是为什么带有副作用的代码会导致无法 Tree Shaking 的原因。
Babel 是一个 JavaScript 编译器,它的主要功能是将新版本的 JavaScript 代码转换成向后兼容的代码。Babel 的工作流程可以简单概括为以下几个步骤:
具体来说,Babel 的工作流程如下:
在整个流程中,Babel 还会使用 babel-preset-env、babel-plugin-transform-runtime、babel-polyfill 等插件和工具来完成更加复杂的任务,如将 ES6 模块转换成 CommonJS 模块,使用 Polyfill 来实现一些新的 API 等。
需要注意的是,Babel 的转换过程是有损的,转换后的代码不一定与原始代码完全相同,也可能存在性能问题。因此,在使用 Babel 进行转换时,需要谨慎选择转换的规则和插件,以确保转换后的代码正确、高效。
Canvas和SVG都可以用于可视化,但它们的优缺点不同。
Canvas: 是一个基于像素的渲染引擎,使用JavaScript API在画布上绘制图像,它的优点包括:
但它也存在一些缺点:
SVG: 是一种基于矢量的图形格式,可以使用XML和JavaScript API在浏览器中绘制图像,它的优点包括:
但它也存在一些缺点:
表格对比
特性 | Canvas | SVG |
图形质量 | 像素级别的图形,适合绘制大量复杂动态的图形糖心 | 矢量图,图形不会失真,适合绘制静态图形 |
图形渲染 | 快速渲染,适合处理大量图形数据 | 慢速渲染,适合处理小规模静态图形 |
交互性 | 事件处理复杂,需要手动编写交互逻辑 | 事件处理简单,内置事件处理机制 |
动画效果 | 动画效果需要手动实现,实现复杂动画困难 | 内置 SMIL 动画支持,可实现较复杂动画效果 |
浏览器支持 | 除 IE8 及以下版本外,其他浏览器都支持 | 除 IE9 及以下版本外,其他浏览器都支持 |
适用场景 | 处理大量动态图形,如游戏开发、数据可视化等 | 绘制简单静态图形,如图标、线条、文字等 |
这个一个较为复杂和庞大的话题, 不能称之为问题, 只能说它是一个话题。
主要涉及到的话题如下:
可以通过根据请求来源(User-Agent)来判断访问设备的类型,然后在服务器端进行适配。例如,可以在服务器端使用 Node.js 的 Express 框架,在路由中对不同的 User-Agent 进行判断,返回不同的页面或数据。具体实现可以参考以下步骤:
具体实现方式还取决于应用的具体场景和需求,以上只是一个大致的思路。