面试知识点汇总

2022/3/1 面试题

前端面试知识点搜集

# HTML

# 如何理解HTML的语义化

根据内容的结构来选择合适的标签。 这样做对搜索引擎友好,seo效果较好;另外,语义化的代码可以让开发者更容易理解代码结构。

# script标签的defer和async都有哪些特点?

<script> 浏览器会立即加载并执行脚本,所以就阻塞了后续dom的执行 => 这就是推荐把script标签引用在dom的最后方 <script defer> 浏览器会立即加载脚本,且不会阻塞dom,等到文档全部加载完后再执行,DOMContentLoaded事件触发前执行 <script async> 浏览器会立即加载并脚本,且不会阻塞dom。即真正的异步加载

# iframe优缺点有哪些?

优点:

  • 适合用来加载速度较慢的部分,不会影响主线的性能
  • 可以实现跨子域通信
  • 可以并行下载脚本
  • 可以实现无刷新上传

缺点:

  • iframe会阻塞onload事件
  • 搜索引擎识别较难
  • 会产生很多页面,难以管理

# 从地址栏输入地址到浏览器打开页面都发生了什么?

  1. 输入URL之后,浏览器解析URL拿到协议、主机名、端口号、路径等信息,然后构建一个http请求
  2. 浏览器访问DNS服务器解析主机名,获得ip地址(DNS查询步骤
  3. TCP链接(三次握手)(三次握手步骤
  4. HTTP请求
  5. 服务器处理请求并返回数据
  6. 浏览器解析并渲染数据(渲染原理
  7. 断开连接(四次握手)(四次握手(挥手)步骤

6、7顺序不一定,比如keep-alive的情况

# CSS

# 讲一下对CSS3盒模型的理解

CSS3的盒模型分两种:标准盒模型和IE盒模型

盒模型分四部分构成:content、border、margin、padding

标准盒模型的width与height不包含border、padding、margin,而IE盒模型包含border和padding不包含margin

css中的box-sizing属性可以修改盒模型,值为border-box时使用IE盒模型,值为content-box时使用标准盒模型

# DOM之间的空白间隔是什么?需要怎么解决?

是换行符等空白字符被浏览器渲染成了一个空格。

解决方法:

  1. 写代码的时候不换行。与习惯差异较大,且不美观
  2. 增加float: left。不是所有的情况都可以用float
  3. 给父元素设置font-size: 0。有的浏览器限制了最小font-size导致不生效
  4. 给父元素设置letter-spacing: -8px。因为子元素会继承,需要给子元素设置letter-spacing: normal

# CSS3 新特性都有哪些?

  • 新增选择器:div:not(.red){} a[attr='a']{}
  • 边框:border-radius、box-shadow、border-image
  • 背景:background-image、background-origin
  • 文本:text-shadow、word-wrap
  • 字体:@font-face{}
  • 变化:transform 的 translate rotate scale skew
  • 过度:transition
  • 动画:@keyframe{}

# CSS如何优化性能?

  • css压缩,减小文件体积
  • 减少使用批量值,例如margin 换成 margin-top
  • 减少使用@important,尽量使用link,可以在文档加载完之前就开始加载
  • 减少无关规则的使用
  • 尽量避免*选择器
  • 降低选择器深度
  • 使用继承的属性
  • 谨慎使用float和position
  • 尽量减少回流和重绘
  • 属性0时不加单位
  • 省略小数点前的0
  • 雪碧图
  • 使用正确的display对应的属性,比如inline的height
  • 优化自定义字体
  • 使用class减小css文件体积
  • css与文档分离

# CSS如何减少回流、重绘

  • 回流(reflow):一旦元素的位置、大小等发生变化时,浏览器会进行的操作
  • 重绘(repaint):一旦元素节点的是否可见切换了,浏览器需要重新绘制像素点的操作

方法:

  1. 减少操作时的css修改,使用class替代
  2. 批量操作DOM,在计算的最后一步处理操作
  3. 使用 absolute、fixed等脱离文档流
  4. 开启GPU加速,使用translate等属性,可以避免reflow

# 讲一下对BFC的理解

Block formatting context,即块级格式化上下文,BFC是一个独立的布局环境,可以理解为一个容器,其中的元素按照规则进行摆放,且不影响外部布局,两个BFC之间分别独立。具体的规则是:

  • 垂直方向自上而下排布
  • 上下两个容器margin会重叠
  • BFC之间互不影响,且容器内不影响外部元素
  • 计算高度时,需要计算浮动元素的高度
  • BFC不会与浮动的容器重叠
  • 每个元素的左margin与容器的左border相接触

BFC解决了以下问题:

  • margin重叠:两个BFC之间互不影响,所以margin就不会重叠
  • 解决高度塌陷:子元素float之后,父元素高度塌陷,设置父元素是BFC即可解决
  • 创建自适应两栏布局:左边宽度固定,右边宽度自适应

BFC使用:

  • body自带
  • float:left、right
  • position:absolute、fixed
  • display:inline-block、flex、table-cell、table-caption等
  • overflow:hidden、auto、scroll

# transition和animation的区别

  • transition是过度属性,需要触发事件,不会自执行
  • animation是动画属性,可以周期性自执行

# 对requestAnimationframe等理解

requestAnimationframe 是动画请求帧,功能类似于setIntervel,但原理是在浏览器进行重绘的时候,增加一个回调函数。 和 clearInterval 类似,也有一个清理方法 clearAnimationFrame。

相比setInterval可能出现的卡顿掉帧情况,requestAnimationFrame相当于自动填写了周期时长(大概1000ms/60hz约等于16.7ms),因为在元素隐藏的时候,避免了回调的执行,相对的减少了dom操作、减少CPU占用,在函数截流等应用下有好处,且因为setInterval在异步队列中,必须要等主线执行完才开始,所以相对较晚执行。

# css省略

.sl{
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowarp;
}
.sl-2{
   overflow: hidden;
   text-overflow: ellipsis;
   display: -webkit-box;
   -webkit-box-orient: vertical;
   -webkit-line-clamp: 2;
}
1
2
3
4
5
6
7
8
9
10
11
12

# 讲一下常用的css单位

  • 像素px:代表最小显示区域,css像素代表一个抽象的相对的单位,物理像素则是设备真实的最小显示区域
  • %:相对于父元素的比例
  • em:当前元素font-size的倍数
  • rem:根元素font-size的倍数
  • vw/vh:分别是屏幕的百分之多少
  • vmin/vmax:vw/vm中的较小/大值

移动端适配更推荐rem/vw/vh配合实现

# 如何实现三栏布局

左右两栏固定,中间自适应。下面共7种方法均可实现。

/* 公共 */
.outer{background: black;height:100px;}
.left{background: tomato;width:100px;height:100px;}
.right{background: gold;width:200px;height:100px;}
.center{background: lightgreen;height:100px}
/* 绝对定位1 */
.outer{position: relative;}
.left{position: absolute;}
.right{position:absolute;right:0;top:0;}
.center{margin: 0 100px;}
/* 绝对定位2 */
.outer{position: relative;}
.left{position: absolute;}
.right{position:absolute;right:0;top:0;}
.center{position:absolute;left:100px;right:100px;top:0;}
/* flex */
.outer{display:flex;}
.center{flex:1}
/* float: center需要放在right后面 */
.left{float:left}
.right{float: right}
.center{margin: 0 200px 0 100px;}
/* float 圣杯布局  */
.outer{padding: 0 200px 0 100px;}
.left{float:left;margin-left: -100px;}
.right{float: left;margin-right: -200px;}
.center{float: left;width: 100%;}
/* float 双飞翼布局 需要给center包一个div */
.left{float:left;margin-right: -100px;}
.right{float: left;margin-left: -200px;}
.center-box{float: left;width: 100%;}
.center{margin: 0 200px 0 100px;}
/* grid */
.outer{display: grid;grid-template-rows:100px;grid-template-columns: 100px auto 200px;grid-gap: 0;}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 1px问题是什么?怎么解决?

1px时在Retina屏幕上,显示1px比较粗,不像1px的情况。因为 像素倍率 = 设备物理像素 / css像素÷÷÷,浏览器中有window.devicePixelRatio属性,当值大于1时,相当于实际像素被放大了。

解决办法:

  1. 直接写0.5px。兼容性很差:ios8+、安卓不兼容
  2. 先放大再用transform缩小。兼容性强,代码多
  3. view-port整体缩放页面。整体缩放就需要考虑其他dom、资源等的展示效果,比较麻烦
<meta name="view-port" content="initial-scale=0.5, minimum-scale=0.5, maximum-scale=0.5, user-scalable=no">
1

# JavaScript

# js的基本类型都有哪些,分别介绍一下

js中共有8种类型,分别是 Undefined、Null、Number、Array、Object、String、Symbol、BigInt。

Null表示空对象,一般用作初始化;Undefined表示未定义,一般用在声明未赋值。

Symbol和BigInt是ES6新增的两种,Symbol类似于uuid,解决全局变量冲突问题,使用Symbol()函数创建;BigInt用来解决Number有范围限制的情况,使用102n以n结尾的数字创建。

获取数据类型的方法有以下几种:

  1. typeof a,存在一个问题是获取Array、Object、Null时都会返回object
  2. a instanceof Object,这个只能判断数据的原型链是否有某一类型,无法判断数据基本类型
  3. a.constructor,可以判断数据类型,但是如果对象的prototype被覆盖,则无法取到原有类型
  4. Object.toString.call(a).slice(8, -1),这个是最确定的方法

引申问题:为什么typeof null === ‘object’?
答案:这是一个历史bug,因为Object的二进制的前三位是000,Null的二进制是00000000,因为前三位是预留来判断数据类型的,所以导致了这个问题

# 箭头函数和普通函数有什么区别?

除了语法简洁外,二者的区别有:

  1. 箭头函数没有this,会指向上一级,且call、bind、apply都无法改变this指向
  2. 箭头函数没有prototype,无法实例化,不能作为构造函数使用
  3. 箭头函数没有arguments
// 指向上级
var b = {a: ()=>{console.log(this)}} 
b.a() // Window {window: …}
// 无法修改this指向
var data = 1
var c = ()=>{console.log(this.data)}
c() // 1 this指向上级,取window.data => 1
c.call({data: 2}) // 1 this无法改变
// 无法实例化
var d = ()=>{console.log(111)}
new d() // Uncaught TypeError: d is not a constructor
// 没有arguments
var e = ()=>{console.log(arguments)}
e(1,2,3) // Uncaught ReferenceError: arguments is not defined
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 数组有哪些原生方法?

方法名 参数 功能 是否改变原数组
无源数组操作
Array.from(arrlike) 传入任意可迭代对象或者有length的参数 创建一个新的数组 无原数组
Array.isArray(arrlike) 传入任意参数 判断是不是数组 无原数组
字符串转换
join(str) 传入连接字符串 把数组用字符串连接起来,返回字符串
toString() 无参数 把数组用逗号连接,返回字符串
entries() 无参数 返回Iterator,项的格式为 [index, item]
keys() 无参数 返回Iterator,项的格式为 index
编辑源数组
fill(item[, start, end]) 数组项, 需要填充的区间,不填默认0~-1 用item填充/覆盖数组项
concat(arr1[... , arrN]) 传入一个以上数组 连接多个数组
shift() 无参数 删除第一项,并返回
pop() 无参数 删除最后一项,并返回
unshift(item) 需要插入的项 向前插入项
push(item) 需要插入的项 向后插入项
copoyWithin(targetIndex(, start, end)) 目标位置索引,被复制区间(start默认为0,end不包含在内)的索引 选择从start到end位的项复制到目标位置
高级操作
slice(start(, end)) 传入起止索引 返回区间内的新数组
splice(start(, len, item1,...itemX)) 开始索引,操作长度,新增的item 在原数组start位开始,选取len长度的项删除,并在start位插入items,返回被删除的数组
循环操作
findIndex((e,i)=>{}) 传入方法,参数e是数组项,i是索引 按照方法规则返回符合条件的第一项的index
find((e,i)=>{}) 传入方法,参数e是数组项,i是索引 按照方法规则返回符合条件的第一项
map((e,i)=>{}) 传入方法,参数e是数组项,i是索引 按照方法格式化数组的每一项,返回格式化后的数组
forEach((e,i)=>{}) 传入方法,参数e是数组项,i是索引 循环遍历数组
filter((e,i)=>{}) 传入方法,参数e是数组项,i是索引 按照规则返回每项都符合条件的新数组
reduce((sum, e)=>{}) 传入方法,参数e是数组项,sum是上一步操作累加值 累加器
reduceRight((sum, e)=>{}) 传入方法,参数e是数组项,sum是上一步操作累加值 反序累加器
判断操作
some((e,i)=>{}) 传入方法,参数e是数组项,i是索引 按照项是否有符合规则的,返回true/false
every((e,i)=>{}) 传入方法,参数e是数组项,i是索引 按照项是否全部符合规则,返回true/false
includes(item) 传入任意参数 判断数组中是否包含该项
indexOf(item) 传入任意参数 判断数组中是否包含该项,并返回第一个符合的索引
lastIndexOf(item) 传入任意参数 反序判断数组中是否包含该项,并返回第一个符合的索引
排序
sort((a,b)=>{}) 传入方法,a、b是项,返回一个数值 根据返回的数值是正(顺序)、负(反序)来操作原数组
reverse() 没有参数 反序原数组

引申问题:
slice、splice比较相似,可能会考二者区别
数组循环操作都有哪些方法?

# 位运算符都有哪些,分别是什么功能?

位运算符是把参数值转换为二进制字符串后再做运算的行为

& 与运算、 | 或运算、^ 异或运算、~ 取反、<< 左移、>> 右移

# 手写一个ajax函数

// 回调版
function ajax(options){
   const {url, type, data, success, error} = options
   const xhr = new XMLHttpRequest();
   xhr.open(type, url, true)
   xhr.onreadaystatechange = function(){
      if(this.readyState != 4) return
      if(this.status === 200){
         success(this.response)
      }else{
         error(this.statusText)
      }
   }
   xhr.onerror = function(){
      error(this.statusText)
   }
   xhr.reponseType = 'json'
   xhr.setRequestHeader("Accept", "application/json")
   xhr.send(data)
}
// promise版
function promiseAjax(options){
   const {url, type, data} = options
   return new Promise(function(res, rej){
      const xhr = new XMLHttpRequest();
      xhr.open(type, url, true)
      xhr.onreadaystatechange = function(){
         if(this.readyState != 4) return
         if(this.status === 200){
            res(this.response)
         }else{
            rej(new Error(this.statusText))
         }
      }
      xhr.onerror = function(){
         rej(new Error(this.statusText))
      }
      xhr.reponseType = 'json'
      xhr.setRequestHeader("Accept", "application/json")
      xhr.send(data)
   })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 常用的请求的方法都有哪些,区别是什么

原生有两种,分别是ajax(XMLHttpRequest)、fetch(ES6新增的使用promise的原生api)

此外还有封装的第三方库 axios(用promise封装的ajax)等

# 什么是尾调用?尾调用有什么好处

尾调用是在函数执行到最后一步调用了另一个函数(return func写在中间也支持),是js自带的优化功能,在ES6中 只有在严格模式下启用。

函数调用是会保留当前函数中的上下文环境,会占用一定内存,调用的层数较多时,性能影响较大,如果函数最后一句调用其他函数,js就不会缓存上下文环境,直接去执行,相当于节省了内存。

递归中也可以使用尾调用,防止递归中出现内存溢出的情况。

# 讲一下你对原型、原型链的理解

js中包含普通对象和函数对象,所有的对象上都有 __proto__ 属性,但是只有函数对象有 prototype:构造函数就是函数对象,构造函数实例化后的对象就是普通对象。

构造函数有 prototype 属性,这个属性指向了调用该构造函数时创建的实例的原型;构造函数实例化后的对象有 __proto__ 属性,这个属性指向了该对象的原型;所以连起来就是:构造函数的prototype属性和实例化对象的__proto__属性相等,即实例的原型

prototype 参数除了手动绑定的参数外,还包含了constructor属性,prototype.constructor指向构造函数本身。 实例化对象的 constructor 属性同样指向构造函数本身。

因为在实例获取属性时如果没有,就会查找原型上的属性,万一原型上没有,就会查找原型的原型,因为原型可以无限层数的实例化,这样形成的链就是原型链。

总结来说:

原型(prototype)是查看当前对象是由哪个构造函数实例化出来的

原型链(__proto__)是一级级地向上层查看实例化原型

引申问题:原型链的终点是什么?
所有构造函数都指向Object,所以原型链的末端指向 Object.prototype.__proto__ 即为 null

# 讲一下你对闭包对理解

概念:闭包是指可以访问另一个函数作用域内变量的函数

功能:创建私有变量,保留变量的内存占用

# 讲一下你对this对象对理解

this是执行上下文的一个属性,指向最后一次调用这个方法的对象。判断具体指向的对象可以根据以下优先级来(从高到低):

  1. 构造函数:实例化时创建了对象,this指向这个变量
  2. apply、call、bind:根据语法绑定this到第一个参数上
  3. 方法调用:如果一个函数作为一个对象的方法被调用时,this指向这个对象
  4. 函数调用:函数被直接调用时,this指向全局对象

# 浏览器中垃圾回收机制都有哪些方式?

垃圾回收是JavaScript代码运行时所占用的内存在不会被使用时解除内存占用的过程;根据JavaScript的特点,JavaScript中的全局变量在页面卸载时会被回收,局部变量在函数执行结束后会被回收,特例时闭包函数中的变量会被外部使用,所以不会在执行结束后被回收。

垃圾回收的方式:1. 标记清除,变量进入执行环境时添加标记,离开时标记为“离开”,然后内存被释放;2. 引用计数,根据变量被引用的次数来计算,当引用次数为0时,释放内存,所以循环引用时不会触发垃圾回收,需要手动处理。

# js是如何体现面对对象的特性的?

面向对象的特性有封装、继承、多态

封装:是隐藏对象的属性,仅提供合理的功能用来外部访问。

在es6之前,js中没有类的概念,但是可以使用函数来模拟类。常见的创建方式有以下几种:

  1. 工厂模式
  2. 构造函数模式
  3. 原型模式

继承:es6之前使用原型prototype实现,子类的原型指向父类的实例,es6使用extend关键字实现继承。

多态:类似设计模式中的适配器模式,父类可以调用不同子类中的同名方法。

// 封装
// 工厂模式
function A(a, b, c){
   var obj = {}
   obj.a = a
   obj.b = b
   obj.c = c
   obj.func = function(){
      console.log(obj.a)
   }
   return obj
}
// 构造函数模式
function B(a, b, c){
   this.a = a
   this.b = b
   this.c = c
   this.func = function(){
      console.log(this.a)
   }
}
// 原型模式
function C(a, b, c){
   this.a = a
   this.b = b
   this.c = c
}
C.prototype.func = funxtion(){
   console.log(this.a)
}
// ES6
class D{
   constructor(a, b, c){
      this.a = a
      this.b = b
      this.c = c
   }
   static getA (){
      return this.a
   }
   sum(){
      return this.a + this.b + this.c
   }
}
// 继承
// 原型链继承
function E(a){
   this.a = a 
}
E.prototype = new A(a, b, c)
E.prototype.constructor = E
b.prototype.func = function(){}
// es6
class F extend D{
   constructor(a, b, c){
      super(a, b, c)
      this.a = a
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

# 异步函数怎么使用?都有哪些方法?

  1. 回调式:容易出现回调地狱,代码高度耦合不利于维护
  2. Promise:链式调用防止回调地狱,但是可能会导致语义不明确,且有个缺点,无法中途取消
  3. generator:步进器一样,把异步写成像回调一样的样子,但是实现比较复杂
  4. async:时generayor和async的语法糖,既可以像同步顺序写异步代码,也可以用promise的特性

# 请说下对Promise和async/await的理解

Promise是一个用来获取异步消息的对象,接收一个函数作为参数,函数中有两个参数res/rej用来表示完成与失败时执行的方法。Promise常用的方法有:then 用来接受成功回调、catch 用来执行失败回调,all 用来执行多个Promise,并等待所有的执行成功触发then 否则触发失败,race 与all类似,哪个先返回使用哪个Promise的回调,无论成功与失败,finally 无论成功失败都会执行的回调;

async/await是Generator的语法糖,是为了优化then的调用链来开发的。async必须包在await函数内,且async返回一个promise对象,即可以使用then等方法;与promise不同的点在于:await会等待promise或者是普通方法返回的结果。

相比primise的链式调用,async/await的写法更偏向于同步代码,也更易理解、调试起来更方便。

# 网络通信

# DNS解析步骤是什么?

  1. 浏览器缓存(浏览器会在缓存里查询有没有主机名对应的ip,查不到就下一步)
  2. 系统缓存(同上,系统中查不到对应的ip就下一步)
  3. 路由器缓存(同上,路由器中查不到对应的ip就下一步)
  4. ISP DNS缓存(去 网络提供商 的DNS缓存服务器中查找,大概率不会走到下一步)
  5. 递归搜索(ISP的DNS服务器开始从root域名服务器开始递归)(如果还没大概率是没有这个网站)

返回 -> 从地址栏输入地址到浏览器打开页面都发生了什么?

# HTTP为什么要三次握手?都发生了什么?

原因:这是由 TCP 的自身特点可靠传输决定的。客户端和服务端要进行可靠传输,那么就需要确认双方的接收和发送能力。

  • 第一次握手,客户端发送一个数据包给服务端,确认客户端的发送能力
  • 第二次握手,服务端返回一个数据包给客户端,确认服务端的接收能力和发送能力
  • 第三次握手,客户端再回传一个数据包,客户端通知服务端准备发送数据

返回 -> 从地址栏输入地址到浏览器打开页面都发生了什么?

# 什么是四次握手(挥手)?

HTTP的连接需要TCP三次握手,而关闭连接则需要TCP四次挥手

  • 第一次挥手,客户端发送给服务端,请求报文已经发送完毕,准备关闭
  • 第二次挥手,服务端发起,请求报文已经接受完毕,准备关闭
  • 第三次挥手,服务器发起,响应报文已经发送完毕,可以关闭
  • 第四次挥手,客户端发起,响应报文接收完毕,可以关闭

第二、三次挥手不一起发送的原因是:第一次握手后服务器知道客户端不再发送数据但还能接收数据,所以自己可以立即关闭 也可以发送数据后再关闭连接,因此确认(步骤2)和关闭(步骤3)是分开的。

返回 -> 从地址栏输入地址到浏览器打开页面都发生了什么?

# 前端安全

# 什么是XSS攻击?如何防御

通过执行恶意脚本,实现获取网站数据、占用服务器资源、流量劫持等目标的行为。常见的操作有:

  1. 存储型:通过把恶意代码提交到数据库,再次打开网站时就会执行恶意代码,把数据发送给攻击者的服务器,一般用来窃取数据;
  2. 反射型:通过在URL中存入恶意代码,当用户被诱导点击进入时,服务端解析过的恶意代码会在浏览器端执行目标操作;
  3. DOM型:通过在URL中存入恶意代码,当用户被诱导点击进入时,浏览器直接根据恶意代码执行操作。

防御方法:

  1. 存入服务器的代码需要转义后再传递
  2. 不使用服务端渲染
  3. 使用CSP
  4. cookie使用http-only,禁止读取

CSP: <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"> 或者服务端添加header,content规则有很多,详细请看MDN CSP文档 (opens new window)

# 什么是CSRF攻击?如何防御

跨站请求伪造攻击,诱导用户进入第三方网站,该网站像被攻击网站发送跨站请求,如果已经登录过,第三方网站就能拿到cookie来冒充用户。

防御方法:

  1. 同源检测:referer、origin,一般不会用,这样会禁止搜索引擎爬虫访问
  2. Token验证:每个请求都要加,比较麻烦,而且有负载均衡时,如果一个用户的请求被分配到两个机子上,就没法验证了
  3. Cookie双重验证:不用多次添加,但是如果有XSS漏洞,就会失效
  4. SameSite:新版chrome已经默认严格模式

# 浏览器原理

# 浏览器解析渲染的原理是什么?

  1. 解析HTML,构建DOM树
  2. 解析CSS,生成CSSOM规则树
  3. 根据解析的DOM树和CSSOM树,构建render树
    • render树的节点就是渲染对象,
    • 渲染对象是一个包含颜色大小等属性的矩形,根据CSSOM规则,不可见节点不会插入到渲染树中
  4. 布局阶段,render树自动重排阶段,根据渲染树来调整元素的定位、尺寸、布局
    • 回流,Reflow,当元素的内容、结构、位置尺寸发生变化时再次布局
  5. 绘制阶段,浏览器遍历渲染树冰调用渲染对象的paint方法,绘制具体像素
    • 重绘,Repaint,当元素的颜色等改变时,再次绘制元素

返回 -> 从地址栏输入地址到浏览器打开页面都发生了什么?

# 浏览器的缓存机制

  1. 第一次访问资源时,浏览器会缓存文件与res header
  2. 再次访问资源时,时间差小于max-age时,读取强缓存(直接读取本地资源)
  3. 如果过期了,开始协商缓存(给服务器发送带有If-None-Match和If-modified-Since的请求)
  4. 接到请求的服务器根据文件的Etag值判断文件是否有修改,没有修改就命中协商缓存,返回304,否则直接返回文件(带上新的Etag)
  5. 如果文件没有Etag,则根据If-Modified-Since对比文件的修改时间,一致则命中协商缓存,返回304,否则返回文件

If-Modified-Since: Fri, 12 May 2006 18:53:33 GMT
浏览器存储缓存的机制:service worker ->> memory cache ->> disk cache

# 性能优化

# 常用的性能优化方法都有哪些?

  1. 静态资源上CDN,CDN会选择响应最快的节点传递给用户
  2. 根据情景选择 图片懒加载/模糊图先加载,清晰图后加载/js预读图片提前加载
  3. css文件预加载 rel="preload" 用来提前加载下一个页面会用到的css文件
  4. 减少回流、重绘
    1. 操作dom时,尽量在最低层级操作
    2. 不要使用table,table会整体重新布局
    3. 减少使用css表达式
    4. 尽量通过操作class来改变样式
    5. 减少dom操作,减少DOM的读/写操作穿插进行
    6. 优化动画,使用absolute等脱离文档流等
    7. js循环插入dom时,使用DocumentFragment存储片段而不是直接插入document里,只进行一次重新渲染
  5. 节流与防抖
    1. 节流:固定时间只触发一次 避免性能问题
    2. 防抖:延迟触发 防止重复 点击/输入 提交
  6. 图片优化
    1. 雪碧图
    2. css代替图片
    3. 使用cdn裁剪合适大小的图片而不是使用原图
    4. 使用 base64 代替小图片
    5. 选择 webP 格式
  7. 使用 HTTP2.0 / HTTP3.0
  8. webpack
    1. 压缩代码
    2. 替换静态资源引用到cdn
    3. 删除多余代码
    4. 按需加载
    5. 提取第三方库

# Vue

# Vue基本原理是什么 / Vue2、3有什么却区别?

Vue2响应式原理: 通过 Object.defineProperty 劫持 get、set,在数据变动时候触发对应的回调

Vue3响应式原理: 基于 Proxy 。

Vue3 相比于 Vue2 在这些方面有优势:

  1. 原生可以监听数组,原来是通过改写数组对原生函数来触发监听函数
  2. 性能提升,原来需要遍历和递归来添加监听,新版本的性能有很大提升
  3. 可以监听到对象属性的增删
  4. 缺点:兼容性只能支持到IE11

原理之外的区别有:

  1. 新增 script 标签上的 setup 属性可以开启 Composition API 的语法使用
  2. Composition API 相比于 Options API:
    1. 可以让代码看起来更内聚,更好的按功能对代码分组;
    2. setup 的生命周期中的 beforeCreated created 不用显式的写出来
  3. 多根节点
  4. 异步组件 suspense,支持预显示 #fallback 插槽内容
  5. 传送组件 Teleport,可以把dom插入到app外,可以解决 display:fixed 可能出现的样式bug
  6. 性能优化,渲染虚拟DOM时 createElementVNode 函数增加了 patchFlag 字段用来标记DOM特征,减少 Diff 时对dom属性的对比,增强性能
  7. 对 TS 的支持更好了

# 双向数据绑定的原理?

MVVM 是 Model–View–ViewModel,就是把网站的视图、数据分离开来,然后通过 ViewModel 来把视图中的变化传递给数据并更新;然后通过数据的更新去更新视图。

Vue2 使用的就是 Object.defineProperty Vue3 通过 Proxy 来实现监听数据更新去渲染视图,然后通过v-model获取视图中的更改来更新数据。

v-model是vue封装了input标签的input事件和value属性的语法糖。

# data 为什么是个函数 而不是个对象?

实际上是可以使用对象,只是会提示出来。

实际上,用函数返回的目的是为了防止组件之间使用了data导致属性之间相互污染的问题

# Computed Methods Watch有什么区别?

  1. Methods是函数,需要手动调用执行,相当于一个工具集合,与vue的数据逻辑无关
  2. Computed是计算属性,相当于监听属性触发的get函数,会立即执行,不用手动出发,且会被缓存
  3. Watch是监听属性更新的回调,不用手动调用,部分情况下需要用deep属性监听,例如层级较深的对象

# 常用的事件修饰符有哪些?

  1. 表单修饰符
    1. v-model.lazy 把input事件改为change事件
    2. v-model.trim 过滤首尾空格
    3. v-model.number 会将输入的值转换为Number
  2. 事件修饰符
    1. @click.stop 阻止冒泡
    2. @click.prevent 阻止默认行为
    3. @click.self 点击自身触发
    4. @click.once 只触发一次
    5. @click.capture 把冒泡更改为由顶层开始向下触发
    6. @click.passive 立即执行默认事件 并懒惰执行事件回调
  3. 鼠标修饰符
    1. @click.left 左键
    2. @click.middle 中键
    3. @click.right 右键
  4. 键盘修饰符 比较多,以按键常规名称显示,可以用 Vue.config.keyCodes = {keyName: keyCode} 来配置
  5. v-bind修饰符
    1. :XXX.async 子组件修改父组件属性的语法糖
    2. :XXX.prop 自定义标签属性

# Vue的声明周期

按照发生对顺序,依次是:

  1. beforeCreated 创建前
  2. created 创建后
  3. beforeMount 挂载前
  4. mounted 挂载后
  5. beforeUpdate 数据更新前
  6. updated 更新后
  7. beforeDestory 销毁前
  8. destroyed 销毁后
  9. activated keep-alive缓存的组件激活时
  10. deactivate keep-alive缓存的组件销毁后

# $nextTick的原理以及作用是什么?

用来在页面dom视图渲染更新后执行的一个回调,一般用在需要使用js操作dom时的特殊情况下。

# 父子组件/组件间的通信怎么实现?

  1. 父传子 props;子传父 $emits,父用v-on监听修改属性值
  2. provide/inject 可以支持跨层级数据下发
  3. 自定义事件总线 EventBus 来传递数据
  4. Vuex Pinia 等插件来更好的管理

# 谈谈你对VueRouter的理解

在传统的多页访问时,路由是浏览器控制的。在单页应用中,管理页面的渲染、跳转逻辑的工具就是路由。

VueRouter有两种模式,分别是 Hash模式 原理是使用浏览器的hash(#)来进行跳转,优点是兼容性更好;History模式 原理上使用HTML5的History API来进行路由管理,优点是更美观。

常用的功能点有 路由守卫,是路由执行过程中触发的钩子函数,例如可以在跳转之前判断用户权限然后控制路由的访问;懒加载,可以把访问频次较低的页面使用异步模式访问,可以不包含在主要的bundle中,可以提高首屏性能;嵌套路由,可以在组件内部的 <router-view> 中通过路由切换。

# 谈谈你对虚拟DOM对理解

虚拟DOM是一种可以渲染成DOM的对象。

真实DOM的操作比较占用资源对性能影响较大,当数据有变更的时候通过新旧虚拟DOM的对比,可以合并DOM的更新进行统一的批量操作、可以更细粒度的更新DOM,这样减少了对性能的消耗。

在虚拟DOM的对比时,程序通过依次对DOM节点的tag、text、attribute、children对比,进行更精准的操作。

# Vue项目的性能优化有哪些?

  1. 区分 v-ifv-show 的使用场景,前者更适合不会高频切换的场景
  2. 区分 computedwatch,computed是计算属性,有缓存 只有在依赖的数据变更的下一次访问时才会更新缓存,watch是监听属性变更,性能开销较大
  3. 使用 v-for 的时候,记得使用key可以提升diff时的性能,并且避免使用v-if,如果必须要用的话 建议使用computed先对数据进行处理后再v-for
  4. 灵活使用Object.freeze,可以冻结不会再变更的对象属性
  5. beforeDestory 时手动销毁 addEventListener 绑定的事件,避免内存泄露
  6. 使用图片懒加载 vue-lazyload
  7. 路由懒加载 import(A.vue),可以增加首页渲染性能
  8. babel-plugin-component 插件可以按需引入三方插件减少项目体积
  9. vue-virtual-scroll 实现长列表的虚拟滚动
  10. SSR
  11. 打包器优化:webpack 增加图片压缩 优化sourceMap 使用webpack-bundle-analyzer具体分析包占用的体积
  12. gzip
  13. 浏览器缓存
  14. cdn
  15. Chrome的性能分析 具体分析

# React

# React与Vue的区别是什么?

  1. 响应式原理不同:Vue是双向数据绑定,Vue2使用Object.definedProperty、Vue3使用Proxy API来处理数据变更;React是单向数据流,使用setState方法来更新数据
  2. 组件状态管理不同:Vue3引入Composition API,管理组件状态;React使用生命周期和Hook来管理
  3. 组件渲染方式不同:Vue使用template;React使用JSX
  4. Diff算法不同:当节点元素类型相同但class不同时,Vue会认为是不同元素重新渲染,React会只处理属性、Vue采取首尾指针法、React采取从左到右一次对比,各有优劣
  5. API风格不同:Vue封装的语法糖较多,相比更加容易上手;React提供的基础API较多,相比更加自由

# setState之后页面发生了什么变化?

  1. 先获取当前的fiber实例、计算本次调度的优先级
  2. 生成Update对象,然后把Update对象加入到fiber的队列中
  3. 然后开始调度更新,先向上遍历寻找到HostRootFiber节点,然后判断路径上节点的子树是否需要更新
  4. 然后开始Render对应的DOM

# React的声明周期

生命周期图示 (opens new window)

挂载阶段:render -> getDerivedStateFromProps -> render -> componentDidMount

更新阶段:getDerivedStateFromProps -> shouldComponentUpadte -> render -> getSnapshotBeforeUpdate -> componentDidUpdate

卸载阶段:componentWillUnmount

具体说明:

  • getDerivedStateFromProps:用来对比新旧props和state和更新state

  • shouldComponentUpadte:通过返回Boolen来控制更新是否渲染来处理性能优化

  • getSnapshotBeforeUpdate:主要用来对比渲染前后DOM的变化,返回的值通过componentDidUpdate的第三个参数使用

  • componentDidMount:首次渲染完毕调用

  • componentDidUpdate:每次更新后调用

  • # componentWillUnmount:销毁前调用,一般用来手动删除addEventListener添加的事件

# React-Router的原理?

React-Router有两种模式,分别是 Hash模式 History模式,Hash模式通过处理hashchange事件来触发路由变化,优点是兼容性较好;History模式通过popstate事件来监听路由变化。

当监听到路由的变化后,React-Router 通知 Router 组件更新location,并且匹配到正确的Route组件去渲染。

# 组件间怎么通信?

  1. 父传子:props
  2. 子传父:
    1. props中使用回调函数,在父组件中可以接收到子组件中传递的值
    2. 使用 useImperativeHandle 暴露函数,父级使用 useRef() 来获取组件并执行组件暴露的函数
  3. 兄弟组件传递:需要先传递给父级 由父级进行转发
  4. 任意组件传递:自定义的事件总线EventBus;或者Redux
  5. 全局数据:挂载在window下;或者自定义全局对象进行读取

# Redux的理解与Vuex有什么区别?

核心概念区别:

Redux:store 单一数据源、reducer 修改state的函数、action 用于描述执行的是什么操作、store.subscribe 用来监听 store 的变化后执行操作

Vuex:state 单一数据源、mutation 修改state的唯一方式、action 用于异步提交mutation、getter 当state 有更新时会重新计算

异步处理区别:

Redux:使用 Action Creator 来异步操作后调dispatch来实现

Vuex:使用 action 执行异步操作后,用 commit 来调 mutation 来更新数据

其他区别:

Redux可以支持任意框架,但 Vuex 只支持 Vue。虽然原理上接近,但是由于支持范围不同,Api风格上略有区别,由于只是针对Vue做的处理,Vuex 的语法上更加的简略,另外Vue上有一个新的状态管理包叫 pinia Api 设计的更加符合直觉。

# React Diff算法的原理

原理是递增法,通过对比新的列表中的节点在原本列表中的位置是否递增,来判断节点是否需要移动。

具体的思路是:先在旧列表中寻找到新列表各项对应的index,然后与上一个节点的index值对比,如果当前位置index大于上个节点index 就说明节点需要移动到前一个节点的后面。

# 前端工程化

# Git和SVN有什么区别

git和svn都是项目代码管理工具,其中svn由于使用的比较少,因为svn不能在离线环境提交代码,创建分支开销较大等;git相对比较主流,但是命令相对比较多,常用的命令有:

  1. git init 初始化一个仓库
  2. git add 暂存一段代码
  3. git rm 删除文件并暂存
  4. git commit 提交暂存的文件
  5. git checkout 创建一个分支
  6. git pull 拉代码
  7. git push 提交代码

我一般使用 vscode 的 git graph 辅助使用git

# 打包工具都有哪些

webpack vite等模块化打包工具,把一切当作模块,根据模块之间的关系构建bundle

grunt gulp等任务执行工具,把资源通过不同的插件加工处理

二者没有可比性,但是现在的单页面应用大都使用 webpack 这类模块化打包工具,多页面应用也可以使用webpack来进行构建

# Webpack的构建流程

  1. 初始化参数,根据配置文件和shell命令中的参数,合并后使用
  2. 开始编译,加载所有的配置插件,开始执行对象的run方法
  3. 确定入口,根据entry的配置找到所有的入口文件
  4. 编译模块,根据入口文件,使用Loader翻译模块,递归调用所有依赖的模块
  5. 完成模块编译,得到每个模块被编译后的最终内容以及依赖关系
  6. 输出资源,根据依赖关系,组成一个个包含多个模块的chunck文件,
  7. 输出完成,根据配置确定输出的路径和文件名,然后写入文件

webpack 会在特定的时间点触发事件,插件监听到事件后执行对应的操作,这个不在构建主流程中,但是会影响结果