由于是笔记类文章,更新比较频繁,后面又重新编排修改了本文 50% 以上的内容,最新内容查看这篇文章:《 「1.5w字总结」Web前端开发必知必会详尽知识手册 》,本文不再更新。
基础部分
CSS盒模型
标准盒模型:box-sizing: content-box
浏览器默认的标准,元素宽度即为内容宽度。
IE盒模型:box-sizing: border-box
元素宽度为内容宽度+边距+边框(content + padding + border = width)
BFC 块级格式化上下文
简单列举几个常见触发条件:
float
不为 noneoverflow
不为 visibledisplay
为 inline-block、table-caption 或 table-cellposition
不为 static 或 relative
应用:
- 阻止
margin
重叠 - 阻止元素被浮动元素覆盖(以前常用于自适应两栏布局)
- 清除内部浮动(父级元素高度塌陷问题)
总结:
BFC 可以视为一种布局的手段,它的目的在于创建出一块独立区域,同时让其内部元素更好地在这片区域中布局。
由于现代 CSS 还在不断发展当中,触发 BFC 的条件可能多达十余种,包括 flex元素、grid元素内也会产生 BFC,又如 display: flow-root
属性值可以创建无副作用的 BFC 等。
回流与重绘
记住引起元素 大小 或 位置 改变的情况,均会触发回流(Reflow)。反之,大小位置不变的情况(如颜色样式color
、background-color
、outline-style
改变),就发生重绘 (Repaint)。
哪些情况会导致回流 (Reflow)
- 页面首次渲染
- 浏览器窗口变化
- 元素尺寸或位置变化(宽高、边距、边框等)
- 元素内容发生变化(文字数量、图片大小、字体大小变化)
- 添加删除可见的DOM节点
- 激活css伪类(hover、active等)
- 查询某些属性或调用某些方法(浏览器会必须回流来保证数据的准确性)
注意:
outline-width
、box-shadow
、border-radius
这些属性并不会引起元素大小的改变,而是样式形状的改变,所以属于重绘。
总结:
- 回流必将引起重绘,重绘不一定引起回流。
- 回流的开销比重绘更大。
思考:
visibility
属性会引起回流还是重绘?答案是只导致重绘,因为 visibility
控制的元素大小位置是不变的。那么同样位置大小不改变的 opacity
呢?其实 opacity
更加特殊一点,因为它触发的是 css3 硬件加速(GPU渲染),所以它既不触发回流也不触发重绘。
常见的触发硬件加速属性有:transform
、opacity
、filters
等。
如何减少回流重绘(性能优化)
HTML层面:
- 避免使用
table
布局 - 在 DOM 树最末端改变 class
CSS层面:
- 尽量减少使用 CSS 表达式(如:
calc
) - 避免多层内联样式
- 将复杂动效应用在脱离文档流的元素上(
position: absolute / fixed
)
JS层面:
- 避免用 JS 操作样式(多个样式改变尽量合并为一次操作)
- 如无法避免多次应用样式或操作 DOM,则可以先设置元素隐藏(先
display:none
再操作) - 重复使用元素属性时赋值给变量(避免重复查询元素导致回流)
DOM事件流及事件委托机制
在如图这样一段 html 结构中,我们点击 button 相当于同时点击了 div、body、以及窗口,所以需要规定事件触发的顺序。
如果直观地认为点击了 button 则应该先触发 button 的事件,外层 div 和 body 于用户而言是无感知的,那么这时的事件流就描述为 冒泡,意为从里向外触发事件。
反之就叫做 捕获 事件流,即无论点击的是什么,都先从最外层触发事件。
我们假设一段DOM结构如下:
<ul>
<li> 1 </li>
<li> 2 </li>
.....
</ul>
如果为每个 li
都赋予点击事件,会注册多个方法,但是给 ul
(父层)中赋予点击事件,利用捕获冒泡的原理,触发 ul
的点击事件时,通过 e.target
判断点击的是哪个 li
,就只需要注册一次方法即可(而且动态添加子节点无需绑定新的事件),这就是JS的事件委托机制。
相关阅读:并不是所有事件都会冒泡
BOM和DOM的区别
BOM 全称是 Browser Object Model,DOM 全称是 Document Object Model,顾名思义 BOM 指的是浏览器对象模型,而 DOM 指的是文档对象模型,属于w3c标准,是所有浏览器都应该遵守的标准。而 BOM 则是由各个浏览器自己扩展的对象模型,实现标准并不相同。
BOM 可以看做指代的是浏览器的 window
对象,DOM 则指的是 window.document
对象,可以看出 DOM 的核心是 BOM 的 window
对象中的子对象 document
(即 BOM 包含了 DOM)。
常见的 window 对象属性:
document
、location
、screen
、history
、frames
什么是Ajax
Ajax 并不指代某种编程语言或技术,它可以看做是一种标准或者思想,区别于传统Web网页应用,它最早提出使用异步JS技术来创建动态的网页,通过与服务器进行少量数据交换,在不重新加载整个网页的情况下来对网页部分内容进行异步动态更新,自2005年开始 Ajax 被大众所接受并逐渐成为主流,直至今天我们的大部分现代网页都在遵循着 Ajax 标准。
狭义的 Ajax 主要关注点在于 XMLHttpRequest
对象,它用于与服务器交互,实现一个 Ajax 实例基本四步走:
function myAjax() {
var xhr = new XMLHttpRequest()
// 处理响应回调
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
...
}
}
}
// 初始化一个请求
xhr.open('post', '/xxx', true)
// 设置请求头信息
xhr.setRequestHeader('Content-type', 'application/json;charset=UTF-8')
// 发送请求
var params = { ... }
xhr.send(params)
}
浏览器基础
浏览器输入URL回车后经过哪些过程
- 读取缓存看能否找到对应IP记录(将域名解析为IP地址)
- 访问DNS服务器(将域名解析为IP地址)
- TCP连接:三次握手
- 发送HTTP请求
- 服务器处理请求并返回HTTP报文
- 浏览器解析渲染页面
- 断开连接:TCP四次挥手
浏览器跨域
- JSONP(利用 script 标签,前端需要定义一个回调函数接收数据,兼容性好,但只能发送 get 请求)
- CORS(与服务端相关)
- PostMessage、WebSocket(HTML5新特性)
- Nginx 反向代理(偏运维知识)
img
标签(知识广度向,加分回答)
移动端屏幕适配
- 利用 meta 标签,
viewport
缩放(页面拉伸模糊) - 响应式布局(css 媒体查询)
rem
,%
,vh vw
等弹性单位(可通过 postcss 插件自动转化px
单位)
Postcss插件如果一定要使用px,如何让部分单位不转换?
- 把那部分 px 写成大写 PX(但代码格式化会fix掉)
- 通过 JS 写入样式(额外触发回流重绘,但是稳定)
浏览器缓存策略
- Cookie:有过期时间,长度限制4kb,且每次都会携带在请求头中,不推荐使用
- SessionStorage:无过期时间,容量大,但窗口关闭自动删除
- LocalStorage:无过期时间,容量大,应用场景很广
- IndexedDB:存储更大量的结构化数据,浏览器本身不限制其容量
网络基础
HTTP 1.0/1.1/2.0 的区别
HTTP 1.0:
- HTTP 协议是无状态的,即同一客户端每次请求都没有任何关系
- 消息结构包含请求头和请求体
HTTP 1.1:
- 引入了持久连接,即 TCP 连接默认不关闭,可以被多个请求复用
- 在同一个 TCP 连接里面,客户端可以同时发送多个请求
- 虽然允许复用 TCP 连接,但是同一个TCP连接里面,所有的数据通信是按次序进行的,服务器只有处理完一个请求,才会接着处理下一个请求。
- 新增了一些请求方法(如 PUT、DELETE 等)、新增一些请求头和响应头
HTTP 2.0:
- 采用二进制格式而非文本格式
- 完全多路复用,而非有序并阻塞的、只需一个连接即可实现并行
- 使用报头压缩,降低开销
- 服务器推送
HTTPS
- 使用了 SSL/TLS 协议进行了加密处理,相对更安全
- 默认端口 443
- 由于需要涉及加密以及多次握手,实际性能会稍逊 HTTP
Get 和 Post 的区别
- Get 传输大小有限,Post 无大小限制
- Get 通过 URL 编码传输数据,Post 通过 body 传输,支持多种编码格式(两者都是明文传输,都不是安全的,但 Get 参数直接暴露在 URL 上,不能用来传递敏感信息)
- 浏览器会缓存 Get 请求,Post 则不会缓存。(在该特性下Get请求可能会出现 304 不更新,解决方法:链接加个随机参数)
网络安全
前端如何防御 XSS(跨站脚本攻击):开启 CSP 即可,副作用是 eval
等方法会失效。
CSS基础
CSS3常用新特性:各种选择器(如:not()
)、圆角、阴影反射、文字特效、线性渐变、旋转,transition
(用于过渡),animation
(用于动画)
水平垂直居中
- flex布局(常用)
.parent {
display: flex;
justify-content: center;
align-items: center;
}
- grid布局(更简洁)
.parent {
display: grid;
place-items: center;
}
3. translate偏移居中(绝对定位中最好用的方法,不定宽高)
.parent {
position: relative;
}
.child {
top: 50%; left:50%;
transform: translate(-50%, -50%);
}
其它的方式实战意义不大建议只了解即可,其中 grid 是最强大的二维布局方式,但只是做居中布局还是一维的 flex 就够用了。
图片居中
- 利用背景实现:
background: url(...) no-repeat center center;
background-size: contain;
2. CSS秒杀背景方案:
object-fit: contain;
有多个属性值可选,这里只写了等比缩放居中的例子,常用的还有
cover
(等比填充居中)
隐藏元素的方法
display: none
:结构消失,触发回流重绘visibility: hidden
:结构保留,占据空间,触发重绘,不可选中opacity: 0
:占据空间,不回流不重绘,可以被选中position: absolute/fixed
:绝对定位元素,设置无限大的负边距将元素抛出视图外clip-path: circle(0px);
:利用裁剪创建元素的可显示区域,区域外会隐藏,占据空间,不可选中(这里我用 circle 是因为它参数最少)
常见问题
为什么图片不能自动撑满?
display:block; // 把img设置为块元素,解决
li与li(或行内块元素)之间看不见的空白间隔?
设置 font-size: 0; // 是受空格影响的,display: inline-block也会产生间隔
css绘制三角形原理?
{
width: 0;
height: 0;
border: 50px solid transparent;
border-top: 50px solid blue;
}
利用边框(border)属性实现,具体为设置div宽高为0,然后设置不同方向的三条边颜色为透明,剩下的边就是三角形
通过变换border宽度调整大小形状,变换剩下那条边的颜色改变三角形颜色。
JS基础训练
相关阅读:手写函数:call、防抖节流
数据类型
基本类型有7种:
string
、number
、null
、undefined
、boolean
、bigint
、symbol
引用类型有:
Object
、Array
- 其中基本类型存放在栈内存中,大小相对是可预期的。
- 引用类型放在堆内存中,主要储存复杂数据。
通常我们可以像以上这么讲,下面说说我的一些思考:
引用类型可能只有一种,在JS中万物皆对象,所以严格上讲数组应该算不上单独类型,可以说数组也属于对象。
并不是所有基本类型都会放在栈内存中,有些存的也是引用值,所以我认为常见的说法只是利用计算机领域的通用概念作出笼统的区分,目的是让初学者更快理解,并不完全准确。 其实在JS中我们只要区分 “值类型” 与 “引用类型” 即可,也仅限在JS中。我们可以确定的事情是 “值类型” 就应该是唯一的,至于一个值类型变量储存的是引用类型的数据时,怎么保证它是唯一的,这是v8引擎做的事,说白了我们根本没接触到真正的底层,所以不必纠结于此。
怎么理解 堆、栈 和 队列 ?
堆 -> 记住就是一个大仓库,申请到一片空间你就可以放任何东西,但是要从里面找东西就比较麻烦,所以需要一份“清单”,通过查找清单上的索引去找你要的那堆东西,就不用每次都进仓库乱翻。
栈 -> 记住就是一个箱子,先放进去的东西反而被压在了箱底,也就最慢才会被拿出来,所以说先进后出,后进先出。
队列 -> 排队都懂吧,讲究一个先来后到,所以肯定先进先出。
this
记住 this
永远指向最后调用它的那个对象
相关阅读:关于 this 常见的 5 种场景
闭包
简单来说就是函数中嵌套函数,这个内部函数暴露给了外部调用。作用是可以访问局部变量,缺点是容易发生内存泄漏(变量不会被自动回收)。
- 可以用来封装私有变量,编写JS库可能会用到
- 实现节流这类函数
- 可以作为缓存数据的一种策略
相关阅读:JS闭包的应用场景
说到内存泄漏,除了闭包以外还有哪些常见的场景会引起内存泄漏?
- 意外的全局变量(全局变量不会被系统自动回收,要注意)
- 被遗忘的定时器(定时器一定要手动销毁,否则常驻内存)
作用域链
以当前环境向上一级一层层查找变量的过程就叫做作用域链。
原型链
每个函数都有 prototype
属性,每个函数实例对象都有一个 __proto__
属性,__proto__
指向了 prototype
,当访问实例对象的属性或方法,会先从自身构造函数中查找,如果找不到就通过 __proto__
去原型中查找。
相关阅读:通过代码理解原型链
call / apply / bind
共同点:都可以改变函数的作用域(箭头函数除外)
call
/ apply
:会立即执行函数,两者区别在于传参不同
bind
:不会立即执行
相关阅读:代码模拟实现一个 call 函数
new关键字
相关阅读:代码模拟 new 一个对象发生的过程
常见问题
说说数组有哪些常见方法?
push:末尾添加
unshift:首部添加
pop:末尾删除
shift:首部删除
concat:数组合并
join:数组元素通过连接符变成字符串
reverse:数组反转
sort:数组排序
flat:数组拍平
slice(start, end):切割,不改变原数组,返回新数组
splice(start, length, newItem):切割,改变数组,从指定位置开始删除,同时可插入新元素
map、foreach、filter、indexOf
相关阅读:foreach 能不能跳出循环?
如何判断数组?
1. Array.isArray([]) // ES6
2. Object.prototype.toString.call([]) // 返回 "[Object Array]"
数组去重?
1. [...new Set(arr)] // ES6
2. 利用 indexOf() 寻找数组下标, -1 表示不存在,根据此规则来得出去重的数组 // ES5
关于深浅拷贝?(首先要区分基本数据类型和引用数据类型)
浅拷贝:
使用新变量等号赋值对象,它们引用相同的地址
深拷贝:
1. JSON.parse(JSON.Stringify()),优点是简单,缺点是不能拷贝Function、undefined会丢失,时间对象会变成字符串。
2. 深度递归遍历
深拷贝(特殊):
Object.assign (一层是深拷贝,二层以上浅拷贝)
手写代码实现深拷贝?
推荐阅读:轻松拿下 JS 浅拷贝、深拷贝(作者:前端阿林)
如何检测数据类型?
typeof 关键字:检测基本数据类型,检测不出 null 和 Array
Object.prototype.toString.call() 方法:可检测所有类型
来做一道经典面试题检测下学习成果吧:
ES6以上常用新语法/特性
- 模板字符串
let
、const
关键字- 箭头函数(没有自己的
this
,不能使用new
命令,不能调用call
) class
类Promise
、async/await
(es7开始支持)export
、import
模块化- 对象扩展(很常用,键值对同名简写,
Object.assign
拷贝或合并对象) - 展开运算符(很常用,
...
用于组装数组/对象) - 解构赋值(可简化提取数组/对象中的值,变量交换不用中间变量)
Map
(与普通对象的区别在于,map键名可以是任意类型,而对象键名只能为字符串)Set
(元素值是唯一的,常用于数组去重)
模块化
- 早期是利用函数自执行实现,在单独的函数作用域中执行代码(如 JQuery )
- AMD:引入
require.js
编写模块化,引用依赖必须提前声明 - CMD:引入
sea.js
编写模块化,特点是可以动态引入依赖 - CommonJS:NodeJs 中的模块化,只在服务端适用,是同步加载
- ES Modules:ES6 中新增的模块化,是目前的主流
JS进阶
继承
JS实现继承就是下面这段代码,ES6专属限定,突出一个面向未来,Java看了直呼内行。
class B extends A {
constructor() {
super();
}
}
要我说es6继承最有实战意义,但要真想搞懂继承,还得继续往下看,首先JS继承到底有哪几种?
- 原型继承
- 构造函数继承
- 组合继承(call / apply)
- 寄生组合继承(最终优化版本,前三种都是推导)
- class继承(es6)
异步编程
EventLoop
JavaScript 是单线程的,包含了同步任务与异步任务,同步任务放入调用栈(主线程)中执行,异步任务会放入消息队列(TaskQueue)中,等待同步任务执行完毕再取出来执行,而此时如果异步之中仍有异步任务,则会继续放入消息队列中等待,这就是JS的事件循环机制(EventLoop)。
Callback
容易造成回调地狱,不能 try...catch
捕获,不能 return
Promise
也会造成回调地狱,但优化了 callback 方式的回调地狱问题,而 async
、await
(ES7)才真正解决了异步回调地狱。
面试高频:
Promise.all()
将一个包含Promise实例的数组传入,数组内所有Promise实例执行完毕时,该方法会**返回结果数组**。
Promise.race()
返回的是最快成功回调的一个结果。
相关阅读:从零开始 - 40行代码实现一个简单Promise函数
推荐阅读:Async / Await 的原理及实现(作者:写代码像蔡徐抻)
宏任务与微任务
异步任务队列中会优先执行微任务,当执行完所有微任务才会执行下一个宏任务。
宏任务:
- I/O操作:这种比较耗性能的操作浏览器会交给单独的线程去办,得到结果后再通知回来
- 定时器系列:
setTimeOut
、setInterval
、requestAnimationFrame
微任务:
- process.nextTick(node)
- Promise.than() / catch() / finaly()
requestAnimationFrame
- 仅对浏览器生效,回调属于高优先级任务
- 会将每一帧中所有DOM操作集中一次渲染
- 重绘或回流的时间会随着浏览器的刷新频率动态改变,不能主动控制(使用时用递归调用,可以中途取消)
- 浏览器页面不是激活状态下,会自动暂停执行
- 根据以上特性该方法常用于处理帧动画操作,而不是使用
setInterval
requestIdleCallback
- 回调属于低优先级任务,仅在浏览器空闲时期被调用(目前仍处于实验功能阶段,在微前端中常有应用)
高阶函数
高阶函数泛指那些操作其他函数的函数。简单来说,就是一个将函数作为参数或者返回值的函数。
例如 Array.prototype.map
、Array.prototype.filter
、Array.prototype.reduce
这些都是JavaScript原生的高阶函数。
函数柯里化
推荐阅读: 柯里化与反柯里化 (作者:我是leon)
流行框架
React与Vue的区别
框架对比我认为是开放问题,每个人都有不同见解,官网是这么描述的:
R:用于构建用户界面的JavaScript库(只提供了UI层面解决方案) V:渐进式JavaScript框架(可以看出致力于构建整体框架生态)
无论你自己的看法如何,首先都可以听听框架的作者的说法,比如 AngularJS
是 Vue
早期开发的灵感来源,React
则有更丰富的生态系统等,这些观点都是出自 Vue
官方文档,尤其我们在面试中如果被问到照着说总不会出错。
React
- JSX 语法
- 单向数据流
- Virtual DOM 控制视图
- Redux状态管理器
- 声明式编程
Vue3
- Object.defineProperty -> Proxy(解决了对象深度监听的问题)
- 重构 Virtual Dom (性能提升)
- 使用 TypeScript
相关阅读:Vue3一些使用差异记录
Vue2
数据双向绑定原理
采用 "发布-订阅" 设计模式,通过 Object.defineProperty() 劫持各个属性的getter、setter,在数据变动时发布消息触发回调更新视图。
Virtual Dom
Model操作,diff算法对比新旧差异,以最小代价转换DOM操作。
组件通信
相关阅读:12 种组件通信方式及理解
实战技巧
Vue视图不更新问题
原因是对象层级嵌套过深或添加了根级数据时发生。Vue3不会有这个问题(proxy解决)。
- 深拷贝覆写对象(不推荐)
$forceUpdate()
强制刷新数据(不推荐,并且失败率高)this.$set
Vue提供的方法,动态添加响应式数据
v-if 和 v-for 共用时控制台报错
当 v-if 与 v-for 一起使用时,v-for 具有比 v-if 更高的优先级( 官网文档 )
正确写法:
<template v-if="show">
<i v-for="item in items">
</template>
组件中的 data 属性为什么不是对象
组件 data 必须是一个函数而不是对象,如果定义成了对象形式,vue 在创建组件的时候 data 变成了引用类型,组件之间的 data 就会共用一个内存地址,所以采用函数定义避免了实例对象之间数据的污染。
.......
data() {
return {
foo: "foo"
}
}
特殊指令:
v-once // 只渲染一次视图
v-pre // 不编译视图,原样输出
事件修饰符:
.stop // 阻止冒泡
.prevent // 阻止默认事件
.self // 仅绑定元素自身触发
.once // 事件只会触发一次
.passive // 不能和 .prevent 一起使用
相关阅读:Vue 处理错误上报原来如此简单
相关阅读:Keep-Alive如何销毁
相关阅读:用好Vue自定义指令让你的开发变得更简单
Vue进阶
相关阅读:从零开始 - 用50行代码实现一个Vuex状态管理器
相关阅读:探索 Vue3 响应式数据原理 ( Proxy 与 Reflect )
推荐阅读: React、Vue2、Vue3的三种Diff算法(作者:Itachi不想说话)
前端性能优化
- 减少 HTTP 请求
- 使用字体图标、svg,避免png图标,如果一定要用 png 图标则采用 css 精灵图(雪碧图)
- 图片懒加载、图片预加载、图片适当压缩质量
- 不使用cookie、iframe、flash(要能说出缺点)
- 避免使用闭包,避免css表达式,减少回流重绘操作(前面有详细介绍到)
- 减少第三方库的引用依赖,对大型框架类库一定要采用按需加载
- webpack 有些压缩混淆去注释的插件一定要配
- css 样式分离在单独的文件中引入(减少内联样式,因为css文件可以被浏览器缓存)
- SPA 应用要使用路由懒加载
- 其它:Gzip压缩、资源CDN、服务端SSR
- 剑走偏锋:一行CSS代码即可提升网页滚动性能
webpack性能优化
从体积角度:
- DllPlugin:可以把不常变动的包提前打出来,在入口文件中引用
- 外部模块CDN:配置enternals可指定其引用域为window
- uglifyJsPlugin:压缩混淆去注释
- 异步加载模块:即路由懒加载,把 import 放到函数中,这样执行时才会引用组件
从速度角度:
- 设置 noParse
- babel-loader 开启缓存,cacheDirectory 减少重新编译的消耗
- 使用 alias 别名设置路径,因为相对路径需要额外递归解析,而别名是绝对路径
推荐阅读:玩转 webpack,使你的打包速度提升 90%(作者:前端瓶子君)
如何实现长列表
- Intersection Observer
- padding
NodeJs
NodeJs虽然偏后端,但是真正作为NodeJs后端开发其实并不多,前端开发同样需要做一些了解,实际工作中的运用我觉得写写脚本搞搞自动化还是很不错的。
工程化
相关阅读:浅谈前端工程化的发展以及相关工具介绍
常见设计模式
单例模式、工厂模式、观察者模式、适配器模式
编程题
相关阅读:常见高频手写题汇总
算法
此时应该怒刷999道力扣算法题,等我先去注册个账号,算法这块我必拿下
我回来了,去提莫的怒刷算法,两道简单就把我给干懵了,不推荐为了面试去刷算法题,如果你和我一样算法这块薄弱,这是 LeetCode 热门题目 100 道,挑一些简单到中等的题目学会就行。
经典排序题
- 冒泡排序
嵌套循环,减减加加,两两交换,代码如下:
for(let a = arr.length; a > 0; a--) {
for(let b = 0; b < a-1; b++) {
arr[b] > arr[b+1] && ([arr[b], arr[b+1]] = [arr[b+1], arr[b]])
}
}
- 插入排序
通过构建有序序列,对未排序数据,在已排序序列中从后向前扫描,找到相应的位置插入。
for(let a = 1; a < arr.length; a++) {
let key = arr[a]
let b = a - 1
while(b >= 0 && arr[b] > arr[a]) {
arr[b + 1] = arr[b]
b--
}
arr[b + 1] = key
}
经典排序只说这两种,首先冒泡是相对比较简单的一种排序方法,然后插入和冒泡稳定性都比较高,两者时间复杂度都是O(n²)。
---------- 持续更新中 ----------
此贴可能不定期更新,实时更新文章链接:book.palxp.com/#/articles/…
以上就是文章的全部内容,感谢看到这里!本人知识水平有限,如有错误望不吝指正,如果觉得写得不错,对你有所帮助或启发,可以点赞收藏支持一下,也欢迎关注,我会更新更多实用的前端知识与技巧。我是茶无味de一天,希望与你共同成长~