面试知识点汇总
前端面试知识点搜集
# HTML
# 如何理解HTML的语义化
根据内容的结构来选择合适的标签。 这样做对搜索引擎友好,seo效果较好;另外,语义化的代码可以让开发者更容易理解代码结构。
# script标签的defer和async都有哪些特点?
<script> 浏览器会立即加载并执行脚本,所以就阻塞了后续dom的执行 => 这就是推荐把script标签引用在dom的最后方
<script defer> 浏览器会立即加载脚本,且不会阻塞dom,等到文档全部加载完后再执行,DOMContentLoaded事件触发前执行
<script async> 浏览器会立即加载并脚本,且不会阻塞dom。即真正的异步加载
# iframe优缺点有哪些?
优点:
- 适合用来加载速度较慢的部分,不会影响主线的性能
- 可以实现跨子域通信
- 可以并行下载脚本
- 可以实现无刷新上传
缺点:
- iframe会阻塞onload事件
- 搜索引擎识别较难
- 会产生很多页面,难以管理
# 从地址栏输入地址到浏览器打开页面都发生了什么?
- 输入URL之后,浏览器解析URL拿到协议、主机名、端口号、路径等信息,然后构建一个http请求
- 浏览器访问DNS服务器解析主机名,获得ip地址(DNS查询步骤)
- TCP链接(三次握手)(三次握手步骤)
- HTTP请求
- 服务器处理请求并返回数据
- 浏览器解析并渲染数据(渲染原理)
- 断开连接(四次握手)(四次握手(挥手)步骤)
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之间的空白间隔是什么?需要怎么解决?
是换行符等空白字符被浏览器渲染成了一个空格。
解决方法:
- 写代码的时候不换行。与习惯差异较大,且不美观
- 增加
float: left。不是所有的情况都可以用float - 给父元素设置
font-size: 0。有的浏览器限制了最小font-size导致不生效 - 给父元素设置
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):一旦元素节点的是否可见切换了,浏览器需要重新绘制像素点的操作
方法:
- 减少操作时的css修改,使用class替代
- 批量操作DOM,在计算的最后一步处理操作
- 使用 absolute、fixed等脱离文档流
- 开启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;
}
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;}
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时,相当于实际像素被放大了。
解决办法:
- 直接写0.5px。兼容性很差:ios8+、安卓不兼容
- 先放大再用transform缩小。兼容性强,代码多
- view-port整体缩放页面。整体缩放就需要考虑其他dom、资源等的展示效果,比较麻烦
<meta name="view-port" content="initial-scale=0.5, minimum-scale=0.5, maximum-scale=0.5, user-scalable=no">
# JavaScript
# js的基本类型都有哪些,分别介绍一下
js中共有8种类型,分别是 Undefined、Null、Number、Array、Object、String、Symbol、BigInt。
Null表示空对象,一般用作初始化;Undefined表示未定义,一般用在声明未赋值。
Symbol和BigInt是ES6新增的两种,Symbol类似于uuid,解决全局变量冲突问题,使用Symbol()函数创建;BigInt用来解决Number有范围限制的情况,使用102n以n结尾的数字创建。
获取数据类型的方法有以下几种:
- typeof a,存在一个问题是获取Array、Object、Null时都会返回object
- a instanceof Object,这个只能判断数据的原型链是否有某一类型,无法判断数据基本类型
- a.constructor,可以判断数据类型,但是如果对象的prototype被覆盖,则无法取到原有类型
- Object.toString.call(a).slice(8, -1),这个是最确定的方法
引申问题:为什么typeof null === ‘object’?
答案:这是一个历史bug,因为Object的二进制的前三位是000,Null的二进制是00000000,因为前三位是预留来判断数据类型的,所以导致了这个问题
# 箭头函数和普通函数有什么区别?
除了语法简洁外,二者的区别有:
- 箭头函数没有this,会指向上一级,且call、bind、apply都无法改变this指向
- 箭头函数没有prototype,无法实例化,不能作为构造函数使用
- 箭头函数没有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
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)
})
}
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是执行上下文的一个属性,指向最后一次调用这个方法的对象。判断具体指向的对象可以根据以下优先级来(从高到低):
- 构造函数:实例化时创建了对象,this指向这个变量
- apply、call、bind:根据语法绑定this到第一个参数上
- 方法调用:如果一个函数作为一个对象的方法被调用时,this指向这个对象
- 函数调用:函数被直接调用时,this指向全局对象
# 浏览器中垃圾回收机制都有哪些方式?
垃圾回收是JavaScript代码运行时所占用的内存在不会被使用时解除内存占用的过程;根据JavaScript的特点,JavaScript中的全局变量在页面卸载时会被回收,局部变量在函数执行结束后会被回收,特例时闭包函数中的变量会被外部使用,所以不会在执行结束后被回收。
垃圾回收的方式:1. 标记清除,变量进入执行环境时添加标记,离开时标记为“离开”,然后内存被释放;2. 引用计数,根据变量被引用的次数来计算,当引用次数为0时,释放内存,所以循环引用时不会触发垃圾回收,需要手动处理。
# js是如何体现面对对象的特性的?
面向对象的特性有封装、继承、多态
封装:是隐藏对象的属性,仅提供合理的功能用来外部访问。
在es6之前,js中没有类的概念,但是可以使用函数来模拟类。常见的创建方式有以下几种:
- 工厂模式
- 构造函数模式
- 原型模式
继承: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
}
}
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
# 异步函数怎么使用?都有哪些方法?
- 回调式:容易出现回调地狱,代码高度耦合不利于维护
- Promise:链式调用防止回调地狱,但是可能会导致语义不明确,且有个缺点,无法中途取消
- generator:步进器一样,把异步写成像回调一样的样子,但是实现比较复杂
- 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解析步骤是什么?
- 浏览器缓存(浏览器会在缓存里查询有没有主机名对应的ip,查不到就下一步)
- 系统缓存(同上,系统中查不到对应的ip就下一步)
- 路由器缓存(同上,路由器中查不到对应的ip就下一步)
- ISP DNS缓存(去 网络提供商 的DNS缓存服务器中查找,大概率不会走到下一步)
- 递归搜索(ISP的DNS服务器开始从root域名服务器开始递归)(如果还没大概率是没有这个网站)
(返回 -> 从地址栏输入地址到浏览器打开页面都发生了什么?)
# HTTP为什么要三次握手?都发生了什么?
原因:这是由 TCP 的自身特点可靠传输决定的。客户端和服务端要进行可靠传输,那么就需要确认双方的接收和发送能力。
- 第一次握手,客户端发送一个数据包给服务端,确认客户端的发送能力
- 第二次握手,服务端返回一个数据包给客户端,确认服务端的接收能力和发送能力
- 第三次握手,客户端再回传一个数据包,客户端通知服务端准备发送数据
(返回 -> 从地址栏输入地址到浏览器打开页面都发生了什么?)
# 什么是四次握手(挥手)?
HTTP的连接需要TCP三次握手,而关闭连接则需要TCP四次挥手
- 第一次挥手,客户端发送给服务端,请求报文已经发送完毕,准备关闭
- 第二次挥手,服务端发起,请求报文已经接受完毕,准备关闭
- 第三次挥手,服务器发起,响应报文已经发送完毕,可以关闭
- 第四次挥手,客户端发起,响应报文接收完毕,可以关闭
第二、三次挥手不一起发送的原因是:第一次握手后服务器知道客户端不再发送数据但还能接收数据,所以自己可以立即关闭 也可以发送数据后再关闭连接,因此确认(步骤2)和关闭(步骤3)是分开的。
(返回 -> 从地址栏输入地址到浏览器打开页面都发生了什么?)
# 前端安全
# 什么是XSS攻击?如何防御
通过执行恶意脚本,实现获取网站数据、占用服务器资源、流量劫持等目标的行为。常见的操作有:
- 存储型:通过把恶意代码提交到数据库,再次打开网站时就会执行恶意代码,把数据发送给攻击者的服务器,一般用来窃取数据;
- 反射型:通过在URL中存入恶意代码,当用户被诱导点击进入时,服务端解析过的恶意代码会在浏览器端执行目标操作;
- DOM型:通过在URL中存入恶意代码,当用户被诱导点击进入时,浏览器直接根据恶意代码执行操作。
防御方法:
- 存入服务器的代码需要转义后再传递
- 不使用服务端渲染
- 使用CSP
- cookie使用http-only,禁止读取
CSP:
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">或者服务端添加header,content规则有很多,详细请看MDN CSP文档 (opens new window)
# 什么是CSRF攻击?如何防御
跨站请求伪造攻击,诱导用户进入第三方网站,该网站像被攻击网站发送跨站请求,如果已经登录过,第三方网站就能拿到cookie来冒充用户。
防御方法:
- 同源检测:referer、origin,一般不会用,这样会禁止搜索引擎爬虫访问
- Token验证:每个请求都要加,比较麻烦,而且有负载均衡时,如果一个用户的请求被分配到两个机子上,就没法验证了
- Cookie双重验证:不用多次添加,但是如果有XSS漏洞,就会失效
- SameSite:新版chrome已经默认严格模式
# 浏览器原理
# 浏览器解析渲染的原理是什么?
- 解析HTML,构建DOM树
- 解析CSS,生成CSSOM规则树
- 根据解析的DOM树和CSSOM树,构建render树
- render树的节点就是渲染对象,
- 渲染对象是一个包含颜色大小等属性的矩形,根据CSSOM规则,不可见节点不会插入到渲染树中
- 布局阶段,render树自动重排阶段,根据渲染树来调整元素的定位、尺寸、布局
- 回流,Reflow,当元素的内容、结构、位置尺寸发生变化时再次布局
- 绘制阶段,浏览器遍历渲染树冰调用渲染对象的paint方法,绘制具体像素
- 重绘,Repaint,当元素的颜色等改变时,再次绘制元素
(返回 -> 从地址栏输入地址到浏览器打开页面都发生了什么?)
# 浏览器的缓存机制
- 第一次访问资源时,浏览器会缓存文件与
res header - 再次访问资源时,时间差小于max-age时,读取强缓存(直接读取本地资源)
- 如果过期了,开始协商缓存(给服务器发送带有If-None-Match和If-modified-Since的请求)
- 接到请求的服务器根据文件的Etag值判断文件是否有修改,没有修改就命中协商缓存,返回304,否则直接返回文件(带上新的Etag)
- 如果文件没有Etag,则根据If-Modified-Since对比文件的修改时间,一致则命中协商缓存,返回304,否则返回文件
If-Modified-Since: Fri, 12 May 2006 18:53:33 GMT
浏览器存储缓存的机制:service worker ->> memory cache ->> disk cache
# 性能优化
# 常用的性能优化方法都有哪些?
- 静态资源上CDN,CDN会选择响应最快的节点传递给用户
- 根据情景选择 图片懒加载/模糊图先加载,清晰图后加载/js预读图片提前加载
- css文件预加载
rel="preload"用来提前加载下一个页面会用到的css文件 - 减少回流、重绘
- 操作dom时,尽量在最低层级操作
- 不要使用table,table会整体重新布局
- 减少使用css表达式
- 尽量通过操作class来改变样式
- 减少dom操作,减少DOM的读/写操作穿插进行
- 优化动画,使用absolute等脱离文档流等
- js循环插入dom时,使用DocumentFragment存储片段而不是直接插入document里,只进行一次重新渲染
- 节流与防抖
- 节流:固定时间只触发一次 避免性能问题
- 防抖:延迟触发 防止重复 点击/输入 提交
- 图片优化
- 雪碧图
- css代替图片
- 使用cdn裁剪合适大小的图片而不是使用原图
- 使用 base64 代替小图片
- 选择 webP 格式
- 使用 HTTP2.0 / HTTP3.0
- webpack
- 压缩代码
- 替换静态资源引用到cdn
- 删除多余代码
- 按需加载
- 提取第三方库
# Vue
# Vue基本原理是什么 / Vue2、3有什么却区别?
Vue2响应式原理: 通过 Object.defineProperty 劫持 get、set,在数据变动时候触发对应的回调
Vue3响应式原理: 基于 Proxy 。
Vue3 相比于 Vue2 在这些方面有优势:
- 原生可以监听数组,原来是通过改写数组对原生函数来触发监听函数
- 性能提升,原来需要遍历和递归来添加监听,新版本的性能有很大提升
- 可以监听到对象属性的增删
- 缺点:兼容性只能支持到IE11
原理之外的区别有:
- 新增 script 标签上的 setup 属性可以开启 Composition API 的语法使用
- Composition API 相比于 Options API:
- 可以让代码看起来更内聚,更好的按功能对代码分组;
- setup 的生命周期中的 beforeCreated created 不用显式的写出来
- 多根节点
- 异步组件 suspense,支持预显示 #fallback 插槽内容
- 传送组件 Teleport,可以把dom插入到app外,可以解决 display:fixed 可能出现的样式bug
- 性能优化,渲染虚拟DOM时 createElementVNode 函数增加了 patchFlag 字段用来标记DOM特征,减少 Diff 时对dom属性的对比,增强性能
- 对 TS 的支持更好了
# 双向数据绑定的原理?
MVVM 是 Model–View–ViewModel,就是把网站的视图、数据分离开来,然后通过 ViewModel 来把视图中的变化传递给数据并更新;然后通过数据的更新去更新视图。
Vue2 使用的就是 Object.defineProperty Vue3 通过 Proxy 来实现监听数据更新去渲染视图,然后通过v-model获取视图中的更改来更新数据。
v-model是vue封装了input标签的input事件和value属性的语法糖。
# data 为什么是个函数 而不是个对象?
实际上是可以使用对象,只是会提示出来。
实际上,用函数返回的目的是为了防止组件之间使用了data导致属性之间相互污染的问题
# Computed Methods Watch有什么区别?
- Methods是函数,需要手动调用执行,相当于一个工具集合,与vue的数据逻辑无关
- Computed是计算属性,相当于监听属性触发的get函数,会立即执行,不用手动出发,且会被缓存
- Watch是监听属性更新的回调,不用手动调用,部分情况下需要用deep属性监听,例如层级较深的对象
# 常用的事件修饰符有哪些?
- 表单修饰符
- v-model.lazy 把input事件改为change事件
- v-model.trim 过滤首尾空格
- v-model.number 会将输入的值转换为Number
- 事件修饰符
- @click.stop 阻止冒泡
- @click.prevent 阻止默认行为
- @click.self 点击自身触发
- @click.once 只触发一次
- @click.capture 把冒泡更改为由顶层开始向下触发
- @click.passive 立即执行默认事件 并懒惰执行事件回调
- 鼠标修饰符
- @click.left 左键
- @click.middle 中键
- @click.right 右键
- 键盘修饰符 比较多,以按键常规名称显示,可以用 Vue.config.keyCodes = {keyName: keyCode} 来配置
- v-bind修饰符
- :XXX.async 子组件修改父组件属性的语法糖
- :XXX.prop 自定义标签属性
# Vue的声明周期
按照发生对顺序,依次是:
- beforeCreated 创建前
- created 创建后
- beforeMount 挂载前
- mounted 挂载后
- beforeUpdate 数据更新前
- updated 更新后
- beforeDestory 销毁前
- destroyed 销毁后
- activated keep-alive缓存的组件激活时
- deactivate keep-alive缓存的组件销毁后
# $nextTick的原理以及作用是什么?
用来在页面dom视图渲染更新后执行的一个回调,一般用在需要使用js操作dom时的特殊情况下。
# 父子组件/组件间的通信怎么实现?
- 父传子 props;子传父 $emits,父用v-on监听修改属性值
- provide/inject 可以支持跨层级数据下发
- 自定义事件总线 EventBus 来传递数据
- 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项目的性能优化有哪些?
- 区分
v-if和v-show的使用场景,前者更适合不会高频切换的场景 - 区分
computed和watch,computed是计算属性,有缓存 只有在依赖的数据变更的下一次访问时才会更新缓存,watch是监听属性变更,性能开销较大 - 使用 v-for 的时候,记得使用key可以提升diff时的性能,并且避免使用v-if,如果必须要用的话 建议使用computed先对数据进行处理后再v-for
- 灵活使用Object.freeze,可以冻结不会再变更的对象属性
- beforeDestory 时手动销毁 addEventListener 绑定的事件,避免内存泄露
- 使用图片懒加载 vue-lazyload
- 路由懒加载
import(A.vue),可以增加首页渲染性能 babel-plugin-component插件可以按需引入三方插件减少项目体积vue-virtual-scroll实现长列表的虚拟滚动- SSR
- 打包器优化:webpack 增加图片压缩 优化sourceMap 使用
webpack-bundle-analyzer具体分析包占用的体积 - gzip
- 浏览器缓存
- cdn
- Chrome的性能分析 具体分析
# React
# React与Vue的区别是什么?
- 响应式原理不同:Vue是双向数据绑定,Vue2使用Object.definedProperty、Vue3使用Proxy API来处理数据变更;React是单向数据流,使用setState方法来更新数据
- 组件状态管理不同:Vue3引入Composition API,管理组件状态;React使用生命周期和Hook来管理
- 组件渲染方式不同:Vue使用template;React使用JSX
- Diff算法不同:当节点元素类型相同但class不同时,Vue会认为是不同元素重新渲染,React会只处理属性、Vue采取首尾指针法、React采取从左到右一次对比,各有优劣
- API风格不同:Vue封装的语法糖较多,相比更加容易上手;React提供的基础API较多,相比更加自由
# setState之后页面发生了什么变化?
- 先获取当前的fiber实例、计算本次调度的优先级
- 生成Update对象,然后把Update对象加入到fiber的队列中
- 然后开始调度更新,先向上遍历寻找到HostRootFiber节点,然后判断路径上节点的子树是否需要更新
- 然后开始Render对应的DOM
# React的声明周期
挂载阶段: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组件去渲染。
# 组件间怎么通信?
- 父传子:props
- 子传父:
- props中使用回调函数,在父组件中可以接收到子组件中传递的值
- 使用 useImperativeHandle 暴露函数,父级使用 useRef() 来获取组件并执行组件暴露的函数
- 兄弟组件传递:需要先传递给父级 由父级进行转发
- 任意组件传递:自定义的事件总线EventBus;或者Redux
- 全局数据:挂载在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相对比较主流,但是命令相对比较多,常用的命令有:
- git init 初始化一个仓库
- git add 暂存一段代码
- git rm 删除文件并暂存
- git commit 提交暂存的文件
- git checkout 创建一个分支
- git pull 拉代码
- git push 提交代码
我一般使用 vscode 的 git graph 辅助使用git
# 打包工具都有哪些
webpack vite等模块化打包工具,把一切当作模块,根据模块之间的关系构建bundle
grunt gulp等任务执行工具,把资源通过不同的插件加工处理
二者没有可比性,但是现在的单页面应用大都使用 webpack 这类模块化打包工具,多页面应用也可以使用webpack来进行构建
# Webpack的构建流程
- 初始化参数,根据配置文件和shell命令中的参数,合并后使用
- 开始编译,加载所有的配置插件,开始执行对象的run方法
- 确定入口,根据entry的配置找到所有的入口文件
- 编译模块,根据入口文件,使用Loader翻译模块,递归调用所有依赖的模块
- 完成模块编译,得到每个模块被编译后的最终内容以及依赖关系
- 输出资源,根据依赖关系,组成一个个包含多个模块的chunck文件,
- 输出完成,根据配置确定输出的路径和文件名,然后写入文件
webpack 会在特定的时间点触发事件,插件监听到事件后执行对应的操作,这个不在构建主流程中,但是会影响结果