一个在教育行业苦干两年的前端菜鸟,离职后就在准备复习面试,记录一下这次前端复习过程。前端知识是在是太多,持续更新中... 希望对真正找工作的你有所帮助,文末我整理了一下面试文章,感兴趣的可以看看,尤其是2021高频前端面试题汇总系列,帮助很大。文章有所不足的地方还指明。
2021前端面试复习
HTML5、CSS3
HTML5
HTML5新增特性
-
语义化标签:
header、nav、footer、section... -
媒体标签:
audio音频、video视频 -
表单类型属性:
email、number、时间控件、color颜色拾取器、placeholder、autofocus自动获取焦点... -
cavas绘图
-
web存储:
localStorage、sessionStorage
行内元素、块级元素有哪些
行内元素:a、span、img、input...
块级元素:div、ul、li、ol、dt、dh、li、p、h1-6
iframe的优缺点
优点:
- 原封不动的吧嵌入网页展示出来
- 增加代码的可重用
- 用来加载速度较慢的内容
缺点:
- iframe阻塞onload事件加载
- 网页内容无法被搜索引擎识别,对SEO不友好
- 会产生很多页面,不利于管理
canvas和SVG的区别
canvas:通过javaScript来绘制2D图形,是逐像素进行渲染
- 依赖分辨率
- 不支持事件处理
- 能够以.png或.jpg的格式进行保存
- 适合做游戏
SVG:基于XML描述的2D图形语言,是矢量图
- 不依赖分辨率
- 支持事件处理
- 适合做大型区域渲染
回流重绘
回流当DOM变化影响了元素,比如元素的尺寸、布局、显示隐藏等改变了,需要重写构建。每个页面至少需要一次回流,就是在页面第一次加载的时候,这个时候一定会发生回流。
重绘当一个元素的外观发生变化,但是没有改变布局,重新渲染元素的外观。比如background-color、color
回流必将引起重绘,而重绘不一定会引起回流
如何避免回流重绘:
- 避免使用
table布局 - 尽可能在
DOM树的最末端改变class - 避免设置多层内联样式
- 开启GPU加速
- 将动画效果应用到
position属性为absolute或者fixed的元素上
src和href的区别
srcsrc指向外部资源的位置,将指向的内容嵌入到文档中当前标签所在的位置,如js脚本、img图片、iframe等
href用于在当前文档和引用资源之间确立联系,一般是用在link、a等元素
CSS3
CSS3新增特性
- 新增CSS选择器、伪类
- 特效:
text-shadow、box-shadow - 渐变:
gradient - 旋转过度:
transform、transtion - 动画:
animation
盒模型
盒模型都是有四部分组成:content、padding、border、margin
标准盒模型和IE盒模型的区别在于设置width和height时,对应的范围不同
-
标准盒模型的
width、height只包含了content -
IE盒模型的
width、height包含了border、margin、padding
通过修改元素的box-sizing属性来改变元素的盒模型
box-sizeing: content-box表示标准盒模型(默认值)box-sizeing: border-box表示IE盒模型(怪异盒模型)
trastion和aniamtion的区别
transtion:属于过度属性,强调过度,需要一个事件进行触发(如鼠标进入、离开)类似flash的补间动画,设置一个开始帧和结束帧
aniamtion:属于动画属性,它的实现不需要触发事件,设定好后可自动执行,且可以循环播放。也是类似补间动画,但是可以设置多个关键帧
元素水平垂直居中
-
绝对定位:先将元素的左上角通过
top:50%和left:50%定位到页面的中心,然后再通过translate来调整元素的中心点到页面的中心。.parent { position: relative; } .child { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); } -
绝对定位:设置四个方向的值都为0,并将
margin设置为auto,由于宽高固定,因此对应方向实现平分,可以实现水平和垂直方向上的居中。.parent { position: relative; } .child { position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; } -
使用
flex弹性盒子布局,通过align-items:center和justify-content:center设置容器的垂直和水平方向上为居中对齐,然后它的子元素也可以实现垂直和水平的居中。.parent { display: flex; justify-content:center; align-items:center; }
p、em、rem的区别
px固定像素单位,不能随其它元素的变化而变化em是相对于父元素的单位,会随着父元素变化而变化rem是相对于根元素html,它会随着html元素变化而变化
rem常用在移动端项目,设置根元素的fong-size,其它元素会随着根元素的变化而变化,从而达到不同手机屏幕的自适应大小。通常会配合postcss-pxtorem插件进行使用
如何解决1px问题
- 直接写
0.5px - 利用伪元素,先放大再缩小
- 使用viewport缩放来解决
什么是BFC布局,如何创建BFC布局?
BFC布局为块格式化上下文(Block Formatting Context,BFC), 是CSS布局的一个概念,里面的元素不会影响到外面的元素。
创建BFC:
- 元素设置浮动:
float有值并不为空 - 元素设置绝对定位:
position(absolute、fixed) overfilow值为:hidden、auto、scrolldisplay值为:inline-block、table-cell、table-caption、flex等
BFC作用:
- 解决
margin重叠问题:由于BFC是一个独立的区域,内部元素和外部元素互不影响,将两个元素变为BFC,就解决了margin重叠问题 - 创建自适应两栏布局:可以用来创建自适应两栏布局,左边宽高固定,右边宽度自适应。
- 解决高度塌陷问题:在子元素设置浮动后,父元素会发生高度的塌陷,也就是父元素的高度为0解决这个问题,只需要将父元素变成一个BFC。
link和@import的区别
link是HTML提供的标签,不仅可以加载CSS文件,还可以定义RSS、rel连接属性等@import是CSS提供等语法规则,只有导入样式表带作用。link标签引入的CSS被同时加载,而@import引入的CSS将在页面加载完毕后被加载@import是CSS2.1才有的语法,存在兼容性,而link作为HTML标签不存在兼容性问题
CSS选择器优先级
@important- 内联样式
- ID选择器
- 类选择器/属性选择器/伪类选择器
- 元素选择器/伪元素选择器
- 关系选择器/通配符选择器
JS基础
基础数据类型
string、number、boolean、object、function、undefined、null、symbol
null和undefined的区别:null表示对是一个空的对象(object)、undefined是申明了但没赋值,在使用typeof检测类型时,null为object,undefined为undefined
null == undefined // true
null === undefined //false
值类型(基本数据类型):string、number、boolean、undefined、null、symbol
引用类型:object、function、array
值类型和引用类型区别:
- 值类型保存在栈中,占用空间固定,当一个方法执行时,每个方法都会创建自己的内存栈,方法中定义的变量会存放在这个内存栈中。当方法执行结束时,这个内存栈也会被销毁。所以,栈中存储的是基础变量。而引用变量存储在栈中是指向堆中的数组或对象的引用地址。这也就是为何修改引用类型总会影响到其它指向这个地址的引用变量。
- 值类型可以使用
typeof进行数据类型检测 - 引用类型保存在堆中,占用空间不固定。创建对象会保存到堆内存中,这个内存不回随着方法结束而销毁,因为这个对象还可能被另外一个变量所引用,只有当一个对象没有被任何变量引用时,系统的垃圾回收机制会将它回收。
- 引用类型使用
instanceof检测数据类型 - 使用new()方法构造出来的对象是引用类型
闭包
闭包就是能够读取到其它函数内部变量的函数,创建一个最简单的闭包,就是在一个函数内部创建另外一个函数,创建的函数可以访问到当前函数的局部变量。
闭包优点:
- 创建全局私有变量,避免变量全局污染
- 可以实现封装、缓存等
闭包缺点:
- 创建的变量不能被回收,容易消耗内存,使用不当会导致内存溢出
变量提升、作用域、作用域链
变量提升
js代码在解析的时候,会将所有的变量函数,提升到代码的最上面。变量提升,提升的只是变量申明,并不会吧变量赋值提升上来
console.log(i) // 4
var i = 4
作用域
作用域是一个变量或函数的可访问范围,作用域控制着变量或函数的可见性和生命周期
-
全局作用域:可以全局访问
- 最外层函数和最外层定义的变量拥有全局作用域
- window上的对象属性方法拥有全局作用域
- 为定义直接复制的变量自动申明拥有全局作用域
- 过多的全局作用域变量会导致变量全局污染,命名冲突
-
函数作用域:只能在函数中访问使用哦
- 在函数中定义的变量,都只能在内部使用,外部无法访问
- 内层作用域可以访问外层,外层不能访问内存作用域
-
ES6中的块级作用域:只在代码块中访问使用
-
使用ES6中新增的
let、const什么的变量,具备块级作用域,块级作用域可以在函数中创建(由{}包裹的代码都是块级作用域) -
let、const申明的变量不会变量提升,const也不能重复申明 -
块级作用域主要用来解决由变量提升导致的变量覆盖问题
var i = 3 function fnc() { console.log(i); var i = 6; } fnc() // undefined
-
作用域链
变量在指定的作用域中没有找到,会依次向上一层作用域进行查找,直到全局作用域。这个查找的过程被称为作用域链。
call、apply、bind区别
- 都可以用作改变
this指向 call和apply的区别在于传参,call、bind都是传入对象。apply传入一个数组。call、apply改变this指向后会立即执行函数,bind在改变this后返回一个函数,不会立即执行函数,需要手动调用。
new操作符干了什么操作
-
创建一个空对象
-
设置原型,将对象的原型设置到函数的
prototype对象上 -
改变this指向,将this指向该对象,并执行构造函数。
-
判断函数的返回类型,如果是值类型,返回创建的对象。如果是引用类型,返回这个引用类型的对象。
原型、原型链
-
原型: 每个对象在内部初始化的时候,都会初始化一个
prototype原型属性 ,而对象的_proto_属性,指向它的原型对象。 -
原型链: 当我们访问英国对象属性时,如果这个属性不存在,那么就会去它的原型对象上进行查找,而这个原型对象又会有自己的原型,会这样一直查找,知道找到顶级对象object为止。这个查找的过程被称为原型对象。
继承
原型链继承
利用对象的原型链,将子类Son.prototype指向父类的构造函数创建出来的实例对象new Person()
🌰
function Person() {
...
};
function Son() {
....
};
// 关键代码
Son.prototype = new Person();
优点:
- 子类可以继承父类构造函数、原型上的属性方法
缺点:
- 父类引用类型的实例对象被共享,容易造成修改的混乱。
- 创建子类的时候不能向父类传参
构造函数继承
利用.call()或者.apply()方法,在子类中借用父类的构造函数,初始化父类构造函数。
🌰
function Person(name) {
...
};
function Son(name) {
// 关键代码
Person.call(this, name)
...
}
优点:
- 子类在继承父类时,可以向父类构造函数中传参。
- 不会造成子类势力之间引用属性共享。
缺点:
- 只能继承父类构造函数中的属性方法,无法访问原型上的方法。
- 每个子类都会创建一个父类副本
组合继承
组合继承,将原型链继承和构造函数继承融合在一起。
🌰
function Person(name) {
...
};
function Son(name) {
// 借用构造函数继承关键代码
Person.call(this, name);
};
// 原型链式继承关键代码
Son.prototype = new Person();
// 将子类的构造函数指向自己
Son.prototype.constructon = Son;
优点:
- 结合前面两种继承方式的优点,子类的实例可以访问到父类原型上的属性方法
- 子类的实例之间不会被共享
缺点:
- 调用了两次父类构造函数
原型式继承
用函数包装一个对象,返回这个函数的调用(也就是ES5的Object.create的模拟实现),将传入的对象作为创建对象的原型
🌰
function create(obj) {
// 创建一个空的的构造函数
function F() {};
// 将空的构造函数原型指向传递进来的对象
F.prototype = obj;
// 返回一个实例对象
return new F();
}
const obj = {
name: 'zs',
...
};
const newObj1 = create(obj);
const newObj2 = create(obj);
优缺点和原型链式继承一样,引用类型还是会被共享。
寄生式继承
在原型式继承基础上,在函数内部来做增强函数,返回对象。
🌰
function createObj(obj){
// 获取继承的子类对象,也就是上面的create方法实现
const newObj = Object.create(obj);
// 函数增强
newObj.say = function() {
...
}
// 返回对象
return newObj;
}
类似原型式继承,但在原有的基础上可以自定义属性方法,依旧没有解决引用值被共享问题。(跟借用构造函数模式一样,每次创建对象都会创建一遍方法。)
寄生组合式继承
结合寄生继承和组合式继承优缺点,组合式继承缺点为调用了两次父类构造函数,优点为解决了引用值被共享问题。而寄生式继承缺点为没有解决引用值被共享问题,只要将两者结合就得到完美的继承方式。
🌰
function createObj(son, parent) {
// 寄生式继承,利用父类构造函数的原型对象创建出一个新的对象,解决组合式继承创建两个父类问题
const newObj = Objcet.create(parent.prototype);
// 将新对象的constructor构造函数执行子类
newObj.constructor = son;
// 将子类构造函数的原型指向心的 原型式继承
son.protoytype = newObj;
}
function Person(name) {
...
};
function Son(name) {
// 构造函数继承
Person.call(this, name);
...
};
// 实现继承
createObj(Son, Person)
// 更简洁的方式 在组合式继承的基础上 直接将son的原型通过API指向person的原型
Son.prototype = Object.create(Person.prototype);
const obj1 = new Son('zx');
const obj2 = new Son('lw');
深拷贝、浅拷贝
浅拷贝
浅拷贝只是复制对象的值类型,通过Object.assign或者扩展运算符即可实现
深拷贝
通过递归实现
function deepClone(obj) {
// 判断是否为对象
if (!obj || typeof obj !=='object') return obj;
// 根据obj类型创建数组还是对象
let newObj = obj instanceof Object ? {} : [];
// 循环遍历obj,处理子元素为对象,递归拷贝
for (key in obj) {
if (obj.hasOwnProperty(key)) {
newOb[key] = deepClone(obj[key])
};
};
return newObj;
};
事件循环机制EventLoop(浏览器)
栈、队列理解
栈(Stack)中的任务后进先出,js中的执行栈是一个存储函数的栈结构,栈中的任务执行遵循先进后出的原则依次执行。队列(Queue)中的任务先进先出,js运行时创建一个任务队列,用来处理列表(事件)和待执行的回调函数
宏观任务、微观任务
js中的任务分为两种:宏观任务(MacroTask|Task)、微观任务(MicorTask)。
- 宏任务:
script全部代码、setTimeout、setInterval、I/O、UI Rendering - 微任务:
Promise.then、Process.nexTick(Node独有)、MutationObserver
同步任务、异步任务
js有一个主线程和一个执行栈(调用栈),所有的任务都会放到执行栈中等待被主线程执行。
js代码在执行时,所有函数都会压入执行栈中。同步任务会按照后进先出原则依次执行,直到执行栈清空。异步任务会在异步任务有了结果后,将注册的回掉函数放入异步任务队列中,等待主线程空闲后(执行栈中的同步任务执行完毕)。
异步队列中的任务又分为宏观任务和微观任务,当当前执行栈清空后,处理异步队列中的任务时,首先会判断是否有微观任务可执行,如果有就将微观任务压入执行栈中执行。当微观队列中的任务在执行栈被执行完毕,再来异步队列中将宏观任务放入执行栈。
简单的来说:
- 执行同步代码,这属于宏观任务
- 所有代码执行完毕,执行栈清空,执行异步队列中任务
- 异步队列中,先执行微观任务
- 微观任务执行完毕,再执行宏观任务
节流防抖
节流(throttle):在n秒内只允许执行一次,
防抖(debounce):在n秒内多次触发但不执行,而是在n秒后执行,如果n秒内触发则重新计算。
事件冒泡、事件委托
事件发生的三个阶段:捕获阶段、目标阶段、冒泡阶段
- 事件冒泡:在一个对象上触发某类事件,如果此对象绑定了事件,就会触发事件,如果没有,就会向这个对象的父级对象传播,最终父级对象触发事件。
- 如何阻止:
- 普通浏览器:
event.stopPropagation() - IE浏览器:
event.cancelBubble = true;
- 普通浏览器:
- 如何阻止:
- 事件委托:利用浏览器事件冒泡机制。事件在冒泡的过程中会传到父节点,并且父节点可以通过事件对象获取到目标节点,可以吧子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件
- 事件委托可以不必要为每一个子节点都绑定监听事件,减少内存上的消耗。
- 使用事件委托还可以实现事件的动态绑定,比如新增一个子节点,并不需要为此单独增加一个监听事件,可以由父元素中的监听函数来处理。
对DOM元素进行增删改查
增
document.createElement()
删
element.removeAttribute()
element.removeChild()
改
element.innerHTML()
element.setAttribute()
查
getElementById()
getElementsByClassName()
getElementsByTagName()
querySelector()
querySelectorAll()
ajax、axios、fetch区别
ajax
- 基于原生的XHR开发
- 本身针对MVC编程,不符合现在前端MVVM潮流
axios
-
从浏览器中创建
XMLHttpRequest -
支持
promise -
支持请求拦击和响应拦截
-
从node.js创建http请求
-
客服端支持防止
CSRF/XSRF
fetch
- 浏览器原生实现的请求方式,ajax的替代品
- 只对网络请求报错,对400、500都当做成功的请求,需要封装
- fetch默认不会带cookie,需要添加配置项
- fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了量的浪费
- fetch没有办法原生监测请求的进度,而XHR可以
ES6
var、let、const区别
var声明变量可以重复声明,而let不可以var是不受限于块级作用域,而let受限var存在变量提升,let和const不存在变量提升const声明的变量不可变const声明之后必须赋值,否则会报错
Promise
Promise是异步编程的一种解决方案,将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。
它有三种状态
pending初始状态fulfilled操作成功rejected操作失败。
Promise状态改变只有两种可能
- 从
pending------>fulfilled - 从
pending------>rejected
Promise构造函数接收一个参数和一个带有resolve和reject参数的回调函数。
resolve的作用是将Promise状态从pending变为fulfilled,在异步操作成功时调用,并将异步结果返回,作为参数传递出去reject的作用是将Promise状态从pending变为rejected,在异步操作失败后,将异步操作错误的结果,作为参数传递出去
Promise实例方法
promise.then()对应resolve成功的处理promise.catch()对应reject失败的处理promise.call()将多个Promise实例,包装成一个新的Promise实例,返回的实例就是普通的Promise。有一个失败,代表该Primise失败。当所有的子Promise完成,返回值时全部值的数组promise.race()类似promise.all(),区别在于有任意一个完成就算完成(例如:将异步和定时器放在一起,设置请求超时)
箭头函数和普通函数的区别
- 箭头函数时匿名函数,不能作为构造函数,不能使用
new - 箭头函数不绑定
arguments - 箭头函数没有自己的
this,将所在的上下文的this作为自己的this值 - 没有
prototype call()、applay()、bind()方法不能改变箭头函数中的this指向
forEach和map的区别
forEach返回值是undefined,不可以链式调用map()返回一个新的数组,不改变原数组。forEach改变原数组。
Set、Map的区别
Set
- 创建:
new Set([1, 1, 2, 3, 3, 4, 2]) add(value):添加某个值,返回Set结构本身。delete(value):删除某个值,返回一个布尔值,表示删除是否成功。has(value):返回一个布尔值,表示该值是否为Set的成员。clear():清除所有成员,没有返回值。
Map
set(key, val):向Map中添加新元素get(key):通过键值查找特定的数值并返回has(key):判断Map对象中是否有Key所对应的值,有返回true,否则返回falsedelete(key):通过键值从Map中移除对应的数据clear():将这个Map中的所有元素删除
区别
Map是一种键值对的集合,和对象不同的是,键可以是任意值Map可以遍历,可以和各种数据格式转换Set是类似数组的一种的数据结构,但在Set中没有重复的值
谈谈你对ES6对理解
- 解构赋值
- 扩展运算符
- 模版字符串
- 箭头函数
async/awaitClass- 引入
Moldule语法 class类
Vue
基础知识
MVVM
MVVM是一种软件架构模式,在vue中 M 代表model层(数据模型),负责数据模型。V代表View层(视图层),VM代表ViewModel(视图模型),它是Model和View之间的桥梁,数据会绑定到viewModel层,并自动将数据渲染到页面层,视图变化时会通知viewModel更新数据
Vue生命周期
创建前后:
beforeCreated(创建前):数据观测和初始化事件还未开始,不能访问data、computed、watch、methods上的数据方法。created(创建后):实例创建完成,可以访问data、computed、watch、methods上的数据方法,但此时渲染节点还未挂在到DOM上,所以不能访问。
挂载前后:
beforeMounted(挂载前):Vue实例还未挂在到页面html上,此时可以发起服务器请求mounted(挂载后):Vue实例已经挂在完毕,可以操作DOM
更新前后:
beforeUpdate(更新前):数据更新之前调用,还未渲染页面updated(更新后):DOM重新渲染,此时数据和界面都是新的。
销毁前后:
beforeDestoryed(销毁前):实例销毁前调用,这时候能够获取到thisdestoryed(销毁后):实例销毁后调用,实例完全被销毁。
watch和computed的区别
watch:监听属性,用来监听数据的变化,没有缓存,当监听到的数据发生变化时都会执行毁掉函数
computed:计算属性,被监听的值有缓存,只有它依赖的属性值发生变化后,下一次获取computed的值时才会重新计算computed的值。(只有依赖发生变化后才会重新计算)
v-for中key的作用
key是为了更高效的对比虚拟DOM中的每个节点是否相同,避免页面更新时重复更新节点
v-if和v-show的区别
v-if元素不可见 删除dom元素
v-show元素可见 通过设置元素的display:none样式属性
组件中的data为什么是一个函数
因为对象是一个引用类型,如果data时一个对象的情况下会造成多个组件共用一个data,data为一个函数,每个组件都会有自己的私有数据空间,不会干扰其他组件的运行。
Vue组件通信
父子组件
父传子
- props
- $children
- $refs
子传父
- $emit
- $parent
兄弟组件
- provied
- inject
- eventBus
- Vuex
Vuex的基本使用
Vuex用于vue中的数据状态管理,有五种属性:
state:Vuex的基本数据,用于存储变量getter:从state派生出来的数据,当相遇state的计算属性,在这里可以对state数据进行过滤、筛选等操作mutation:提交更新state数据的方法action:和mutation功能相似,都是用来提交更新,但action提交的是mutation,而不是直接变更数据,并且action可以包含异步操作module:模块化Vuex,每个模块都有自己的state、mutation、actoion、getter
mutation和action的区别
mutation更专注于修改state,必须是同步执行。action提交的是mutation,而不是直接更新数据,可以是异步的。action可以整合多个mutation
Vuex和localstory的区别
Vuex存储在内存中,页面关闭刷新就会消失。而localstorage存储在本地,读取内存比读取硬盘速度要快Vuex应用于组件之间的传值,localstorage主要用于不同页面之间的传递Vuex是响应式的,localstorage需要刷新
路由守卫
- 全局前置钩子:
beforeEach、beforeResolve、afterEach - 路由独享守卫:
beforeEnter - 组件内钩子:
beforeRouterEnter、beforeRouterUpdate、beforeRouterLeave
hash和history的区别
hash
hash模式是vue开发中的默认模式,地址栏URL携带#,#后为路由。
原理是通过onhashchange()事件监听路由hash的变化,这个好处就是当hash值发生变化,不需要向后端发起请求,window就可以监听事件的改变,并按照规则加载项对应的代码。除此之外,hash值的变化对应的URL都会被浏览器记录下来,这样就能实现浏览器历史页面的前进后退。
history
vue还提供history模式,在history模式下URL中没有#,相比hash模式更加好看。但是需要后台配置支持。
history的原理是利用HTML5中hostory提供的pushState、replaceState这两个API,这两个API记录了浏览器历史栈,并且当在修改URL时不会触发页面刷新和后台请求。
动态路由
定义方式
- params传参
- 路由配置:
/index/:id - 路由跳转:
this.$router.push({name: 'index', params: {id: "zs"}}); - 路由参数获取:
this.params.id - 最后形成的路由:
/index/zs
- 路由配置:
- query传参
- 路由配置:
/index正常的路由配置 - 路由跳转:
this.$rouetr.push({path: 'index', query:{id: "zs"}}); - 路由参数获取:
this.query.id - 最后形成的路由:
/index?id=zs
- 路由配置:
route
$router是指整个路由对象,可以使用this.$router.push({name: ;index'})进行页面跳转$route时指当前页面的路由对象,可以使用this.$route.parmas.id来获取当前路由对象传递进来的参数
Vue性能优化
原理知识
双向绑定原理
data在初始化的时候,会实例化一个Observe类,在它会将data数据进行递归遍历,并且通过definereactive方法,这个方法通过Object.defineProperty方法,给每个值添加上一个getter和一个setter。在数据读取的时候会触发getter进行依赖(Watcher)收集,当数据改变时,会触发setter,对刚刚收集的依赖进行触发,并且更新watcher通知视图进行渲染。
依赖收集
依赖收集发生在defineReactive()方法中,在方法内new Dep()实例化一个Dep()实例,然后在getter中通过dep.depend()方法对数据依赖进行收集,然后在settter中通过dep.notify()通知更新。整个Dep其实就是一个观察者,吧收集的依赖存储起来,在需要的时候进行调用。在收集数据依赖的时候,会为数据创建一个Watcher,当数据发生改变通知每个Watcher,由Wathcer进行更新渲染。
Object.defineProperty()数据劫持缺陷
该方法只能监听到数据的修改,监听不到数据的新增和删除。vue2中会对数组的新增删除方法push、pop、shift、unshift、splice、sort、reserve通过重写的形式,在拦截里面进行手动收集触发依赖更新。
在vue2中,需要数据里添加或删除时,使用vue.$set/vue.$delete进行操作。
在Vue3中,改用proxy对对象进行代理,返回一个代理对象,只需要操作新对象就可以。
双向绑定原理
Vue双向绑定是一个指令v-model,可以将数据动态绑定到视图上,同时视图中变化也可以改变改值。他的本质是 v-bind 和 v-on 的语法糖。在 ⼀个组件上使⽤ v-model ,默认会为组件绑定名为 value 的 prop 和名为 input 的事件。
nextTick的实现
vue中的nextTick是浏览器eventLoop是应用。nextTick是将回调函数放到一个异步队列中,保证在异步更新DOM的watcher后面,从而获取到更新后的DOM。
Vue在更新DOM时是异步执行的。只要侦听到数据变化,Vue将开启1个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作是非常重要的。nextTick方法会在队列中加入一个回调函数,确保该函数在前面的dom操作完成后才调用;
怎么理解 vue 中的虚拟 DOM
虚拟DOM,就是用一个JS对象来描述一个DOM节点。Vue是数据驱动视图的,数据发生变化视图就要随之更新,在更新视图的时候难免要操作DOM,而操作真实DOM又是非常耗费性能的,这是因为浏览器的标准就把 DOM 设计的非常复杂,所以一个真正的 DOM 元素是非常庞大的。VNode类中包含了描述一个真实DOM节点所需要的一系列属性,tag表示节点的标签名,text表示节点中包含的文本,children表示该节点包含的子节点等。
模版编译原理
模版编译主要过程:template ---> ast ---> render,分别对象三个方法
parse函数解析templateoptimize函数优化静态内容generate函数创建render函数字符串
调用parse方法,将template转化为AST(抽象语法树),AST定义了三种类型,一种html标签,一种文本,一种插值表达式,并且通过 children 这个字段层层嵌套形成了树状的结构。
optimize方法对AST树进行静态内容优化,分析出哪些是静态节点,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化。
generate将AST抽象语法树编译成 render字符串,最后通过new Function(render)生成可执行的render函数
diff算法逻辑
diff算法发生在视图更新阶段,也就是当数据发生变化的时候,diff会对新就虚拟DOM进行对比,只渲染有变化的部分。
当数据发生变化的时候,依赖对应的watcher会通知更新,生成一个新的vnode,新的vnode会去和旧的vnode进行对比更新。
整个更新的过程就是调用path函数,主要做了三件事:
- 创建节点:新的
vnode中有而旧的vnode中的节点,在旧vnode中进行创建 - 删除节点:新的
vnode中没有二旧的vnode中有,在旧的vnode中删除 - 更新节点:新的
vnode和旧的vnode中都有,以新的vnode位主,更新旧的vnode
new Vue的流程
合并配置,调用一些初始化函数,触发生命周期钩子函数,调用$mount开启下一个阶段。
keep-live原理
keep-alive是Vue.js的一个内置组件。它能够将不活动的组件实例保存在内存中,而不是直接将其销毁,它是一个抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中。
通过include、exclude来匹配和排除缓存,max定义缓存的上限。
keep-alive内部其实是一个函数式组件,没有template标签。在render中通过获取组件的name和include、exclude进行匹配。匹配不成功,则不需要进行缓存,直接返回该组件的Vnode。
匹配成功就进行缓存,获取组件的key在this.cache中进行查找,如果存在就直接将缓存的组件实例覆盖到当前的Vnode上,然后将当前组件的key从keys中进行删除,然后在push(key)添加到尾部,这样做是为了改变key当前的位置,也就实现了max功能。
不存在的话,就需要对组件进行缓存。将当前组件push(key)添加到尾部,然后再判断当前缓存的max是否超出指定个数,如果超出直接将第一个组件销毁(缓存淘汰策略LRU)。
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
性能优化
网站优化
- 使用浏览器缓存机制
- 资源懒加载、预加载
- 骨架屏
- 合理使用雪碧图、字体图标、
代码优化
- 减少回流重绘
- 减少DOM的操作
- 节流防抖
- 使用事件委托
- 将CSS文件放在头部、js文件放在底部
请求优化
- 使用CDN加速
- 开启nginx,Gzip压缩
- 使用强缓存、协商缓存
- 减少、合并请求
webpack优化
- 按需加载
- 代码打包体积压缩
- 移除console
- 压缩图片、字体等本地资源
- 分离css文件,单独进行打包
浏览器相关
跨域
浏览器的同源策略会导致跨域问题
同源策略
同源指的是:协议、端口号、域名必须一致。
同源策略的目的主要是为了保证用户的信息安全,它只是对 js 脚本的一种限制,并不是对浏览器的限制,对于一般的 img、或者script 脚本请求都不会有跨域的限制,这是因为这些操作都不会通过响应结果来进行可能出现安全问题的操作
解决跨域问题
- CORS:服务器开启跨域资源共享
- JSONP:利用JavaScript标签不存在跨域限制,只支持GET请求
- Nginx:反向代理
本地存储
Cookie
- 存储小,只有4k
- 不同域之间不能共享
- 不安全,容易被拦截
SessionStorage
- 存储在内存中,体积相对较大
- 页面关闭,数据会删除
- 相对Cookie安全
LocalStorage
- 体积大,可以存储更多内容
- 生命周期长,需要手动删除
- 存储在硬盘,不会像cookie一样被请求携带
从输入一个 URL 地址到浏览器完成渲染的整个过程
- 浏览器输入
URL并回车 - 浏览器查找当前是否有缓存
DNS解析URL得到IP- 浏览器
DNS缓存 host文件缓存
- 浏览器
- 建立
TCP连接(三次握手) - 发送
HTTP请求 - 服务器处理请求,浏览器得到响应数据
- 浏览器解析渲染页面
- 解析
DOM生成DOM树 - 解析
CSS生成CSS树 - 合并
DOM树和CSS树,生成渲染树 - 浏览器开始渲染页面(回流重绘发生在这个阶段)
- 解析
TCP连接关闭(四次挥手)
网络协议、安全相关
TCP、UDP协议
TCP和UDP都是在传输层定义的两种传输协议。基于UDP协议传输不能保证数据准确无误的送达,但UDP不仅可以支持一对一的传输方式,还可以支持一对一、一对多等形式。也不需要像TCP一样建立连接,所以传输速度快。
TCP的目的是提供可靠的数据,并且需要在传输前建立连接(三次握手)。只支持一对一进行传输。
区别:
TCP协议可靠,UDP协议不可靠TCP面向连接,UDP采用无连接TCP可以保证数据顺序,UDP不能TCP一对一传输,UDP可以一对多、多对一等形式
HTTP和HTTPS区别
HTTP是明文传输,不安全。HTTPS基于SSL进行加密传输,比较安全。HTTPS需要CA证书,HTTP不需要。HTTP端口为80,HTTPS端口为443
HTTP状态码
- 1XX: 请求正在处理
- 2XX:正常状态码
- 200 :请求处理成功
- 201 : 请求成功并且服务器创建了新资源
- 202 :服务器已经接收请求,但尚未处理
- 3XXX:重定向状态
- 301 :请求重定向
- 302: 临时重定向
- 303: 临时重定向,使用get请求新的url
- 304:浏览器缓存相关
- 4XX:错误状态码
- 400: 服务器无法理解请求格式,需要修改请求内容后再次发起请求
- 401: 请求未授权
- 403: 禁止访问
- 404: 服务器上无法找到请求资源
- 5XX:服务器错误
- 500: 服务端错误
- 503: 服务器暂时无法处理请求
HTTP三次握手、四次挥手
三次握手
三次握手是在建立TCP连接时,客户端和服务端总共发送三个包。进行三次握手的主要目的就是为了确认双方的接受能力和发送能力都是正常的,为后面传输可靠数据做准备。
报文:
- 序号:表示发送的数据字节流,确保TCP传输有序,对每个字节编号
- 确认序号:发送方期待接收的下一序列号,接收成功后的数据字节序列号加 1。只有
ACK=1时才有效。 ACK:确认序号的标志,ACK=1表示确认号有效,ACK=0表示报文不含确认序号信息SYN:连接请求序号标志,用于建立连接,SYN=1表示请求连接FIN:结束标志,用于释放连接,FIN=1表示关闭本方数据流
三次握手:
- 第一次握手:客户端给服务端发一个
SYN报文,并指明客户端的初始化序列号ISN,此时客户端处于SYN_SEND状态。 - 务器收到客户端的
SYN报文之后,会以自己的SYN报文作为应答,并且也是指定了自己的初始化序列号ISN。同时会把客户端的ISN + 1作为ACK的值,表示自己已经收到了客户端的SYN,此时服务器处于SYN_REVD的状态。 - 客户端收到
SYN报文之后,会发送一个ACK报文,当然,也是一样把服务器的ISN + 1作为ACK的值,表示已经收到了服务端的SYN报文,此时客户端处于ESTABLISHED状态。服务器收到ACK报文之后,也处于ESTABLISHED 状态,此时,双方已建立起了连接。
四次挥手
- 客户端会发送一个
FIN报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT1状态。 - 服务端收到
FIN之后,会发送ACK报文,且把客户端的序列号值 +1 作为ACK报文的序列号值,表明已经收到客户端的报文了,此时服务端处于CLOSE_WAIT状态。 - 如果服务端也想断开连接了,和客户端的第一次挥手一样,发给
FIN报文,且指定一个序列号。此时服务端处于LAST_ACK的状态。 - 客户端收到
FIN之后,一样发送一个ACK报文作为应答,且把服务端的序列号值 +1 作为自己ACK报文的序列号值,此时客户端处于TIME_WAIT状态。需要过一阵子以确保服务端收到自己的ACK报文之后才会进入CLOSED状态,服务端收到ACK报文之后,就处于关闭连接了,处于CLOSED状态。
为什么需要四次挥手
因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。
HTTP缓存
强缓存
使用强制缓存策略,如果缓存资源有效,就直接使用缓存资源,不需要向服务器发送请求。强制缓存通过两种方式来设置,在request headers中的Expires属性和Cache-Contorl属性。
Expires属性,指定资源的过期时间。在过期时间以内,改资源可以被缓存使用,不需要向浏览器发送请求。这个时间依赖于服务器时间,会存在服务器时间和客户端时间不一致。
Cache-Control属性:
private: 仅浏览器可以缓存public:浏览器和代理服务器都可以缓存max-age=xxx过期时间,单位为秒no-cache不进行强缓存,但会有协商缓存no-store不强缓存,也不协商缓存
如果request header中,Cache- Control的值中有max-age=xxx,这个时候走强缓存。如果值为no-cache,表明没有命中,走协商缓存。如值为no-store,不使用缓存。
协商缓存
如果没有命中强制缓存,在设置协商缓存情况下,先向服务器发送一个请求,如果资源没有发生修改,则返回一个304的状态,让浏览器使用本地的缓存副本。如果资源发生修改,则返回修改后的资源。在request headers中的Etag属性和Last-Modified属性,来进行设置。其中,ETage优先于Last-Modified。
命中协商缓存条件:
Cache-Control: no-cachemax-age时间过期
Last-Modified(文件的修改时间):
服务器在响应头中添加Last-Modified属性,来指出资源最后一次修改时间。当浏览器发起请求时,会在request headers中添加一个If-None-Match属性,值为上一次请求的资源返回的Last-Modified值。服务端会通过这个属性和资源最后一次修改时间进行对比,以此来判断资源是否修改。如果资源没有修改,请求返回304状态,客户端使用本地缓存。如果资源有修改,则返回修改的资源。
这种方式有一个缺点,Last-Modified标记的时间只能精确到秒。
ETag(文件改动):
同样在服务器返回资源的时候,在头信息中添加ETag属性,这个属性是资源的唯一标识符。当资源改变时,这个值也会改变。在一下次请求资源时,会在request headers中添加一个If-None-Match属性,值为上一次请求的资源返回的ETag值。服务端会通过这个属性和资源最后一次修改时间进行对比,以此来判断资源是否修改。这种方式比Last-Modified更加准确。
区别
- 强缓存优先级高于协商缓存
- 强缓存不需要发请求,协商缓存需要。
- 强缓存返回的状态码为
200,协商缓存返回304 - ctrl+F5强制刷新会跳过所有缓存,而F5刷新跳过强缓存,但是会检查协商缓存。
POST和GET的区别
- 传递的参数不同,
POST传递的参数在request body中,GET传递的参数在url后拼接 POST相对GET请求安全GET请求长度有限制,POST没有GET请求会被浏览器主动缓存,POST不会,要手动设置GET请求一般用于查询,POST一般用于提交某种信息进行某些修改操作
XSS、csrf攻击
XSS(跨站脚本攻击)
Xss(cross-site scripting) 是一种代码注入攻击,攻击者往Web页面里插入恶意 html 标签或者 javascript 代码。在骗取用户点击后获取用户,获取用户信息。
避免方式:
url参数通过encodeURIComponent方法进行转义- 尽量不使用
InnerHtml插入HTML内容 - 对用户输入的地方和变量都需要仔细检查长度和对 ”<”,”>”,”;”,”’” 等字符做过滤
CSRF(跨站请求伪造)
CSRF(Cross-site request forgery)攻击者盗用你的身份信息,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。
避免方式:
- 添加验证码验证
- 使用
token- 服务端给用户生成一个
token,加密后传递给用户 - 用户在提交请求时,需要携带这个
token - 服务端验证
token是否正确
- 服务端给用户生成一个
前端工程化
webpack的loader和plugin的区别
loader
loader是导出一个函数的javascript模块,webpack原生是只能解析js文件,如果想将其他文件也打包的话,就会用到loader。如babel-loader、Css-loader、image-loader、url-loader、Saas-loader...
plugin
Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。如html-webpack-plugin、mini-css-extract-plugin、uglifyjs-webpack-plugin
手写系列🤮
节流防抖
new操作符
call、bind、apply实现
发布订阅模式
class EventEmitter {
constructor() {
// 事件列表
this.eventList = {};
}
// 监听
on(name, callBack) {
// 以 name 为 key 创建容器 如果有容器就不用创建
if (!this.eventList[name]) {
this.eventList[name] = []
}
// 把事件放入容器
this.eventList[name].push([callBack])
};
// 触发
emit(name, data) {
if (!this.eventList[name]) {
return new Error('没有找到事件!')
};
// 从容器中取出事件进行调用
this.eventList[name].forEach((item) => {
item(data)
})
};
// 只触发一次
once(name, callBack) {
if (!this.eventList[name]) return;
// 利用off 在callBack执行后 关闭订阅
function onceFn(callBack) {
callBack();
this.off(name, callBack);
};
this.on(name, onceFn)
};
// 关闭监听 若第二个参数没有 移除 name 下所有的事件
off(name, callBack) {
if (!this.eventList[name]) return;
if (callBack) {
// 只移除对应的callBack
this.eventList[name] = this.eventList[name].filter((item) => {return item !== callBack});
// name容器长度为0 直接删除整个 name 事件订阅发布
if (this.eventList[name].length === 0) {
delete this.eventList[name];
};
} else {
// 没有 callBack 直接删除整个 name 事件订阅发布
delete this.eventList[name];
};
};
};
函数柯里化实现
promise
实现一个队列
数组去重
深浅拷贝
交换两个变量的值
数组扁平化
输出结果
闭包
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //"The Window"
function aaa(){
var a=0;
function bbb() {
a++;
alert(a);
}
return bbb
}
var ccc=aaa();
ccc(); //结果为1
ccc(); //结果为2
var ddd=aaa();
ddd(); //结果为1
ddd(); //结果为2
参考: