八股合集
🌙HTML
⭐iframe优缺点
优点:
- 网页嵌套:通过 iframe,可以将另一个网页嵌入到当前页面中。这对于集成第三方内容(如地图、视频、社交媒体插件等)非常方便,可以在不离开当前页面的情况下展示外部内容。
- 安全隔离:每个 iframe 都有独立的文档对象模型(DOM),这意味着它们之间的操作相互隔离。这可以防止外部网页影响当前页面的样式和脚本。
- 并行、异步加载:iframe 可以并行且异步加载其嵌套的网页,不会阻塞当前页面的渲染和加载。这可以提高页面的整体性能和加载速度。
缺点:
- 性能问题:每个iframe都需要加载渲染,会增加页面加载时间和资源消耗
- SEO 问题:搜索引擎可能无法正确地解析 iframe 内的内容,这可能会影响页面的搜索引擎优化(SEO)。
- 跨域限制:由于浏览器的同源策略限制,嵌套在 iframe 中的网页只能与同域的父页面进行受限的通信。跨域通信需要通过特殊的技术手段(如跨文档消息传递)来实现。
- 响应式问题:在移动设备等小屏幕上,iframe 可能需要滚动或者缩放才能适应屏幕,这可能会导致用户体验下降。
⭐H5新特性
- 新增选择器 document.querySelector、document.querySelectorAll
- 拖拽释放(Drag and drop) API
- 媒体播放的 video 和 audio
- 本地存储 localStorage 和 sessionStorage
- 离线应用 manifest
- 桌面通知 Notififications
- 语意化标签 article、footer、header、nav、section
- 增强表单控件 calendar、date、time、email、url、search
- 地理位置 Geolocation
- 多任务 webworker
- 全双工通信协议 websocket
- 历史管理 history
- 跨域资源共享(CORS) Access-Control-Allow-Origin
- 页面可见性改变事件 visibilitychange
- 跨窗口通信 PostMessage
- Form Data 对象
- 绘画 canvas
⭐自定义元素
HTML 提供了一系列内置的元素(如 <div>
、<p>
等),而自定义元素则是由开发者自定义的,可以用来扩展 HTML 语义和功能。
在传统的 HTML 中,我们只能使用已定义的标签来创建元素,如 <div>
、<p>
、<span>
等。但随着 Web 技术的不断发展,HTML5 引入了自定义元素的功能,允许开发者创建自己的标签,并赋予其特定的行为和样式。
🌙CSS
⭐BFC
带你用最简单的方式理解最全面的BFC哔哩哔哩bilibili
BFC,即块级格式化上下文(Block Formatting Context),是CSS中的一个概念。它是指一个独立的渲染区域,其中的元素按照一定的规则进行布局和渲染。
BFC的主要作用是控制元素在布局上的行为,包括外边距的塌陷、浮动元素对周围元素的影响以及清除浮动等。以下是BFC的一些特性和应用:
- 外边距折叠:在BFC中,垂直方向上的相邻元素的外边距会发生折叠(合并)现象,从而避免了不必要的间距。但在同一个BFC的元素中,父子元素之间的外边距不会发生折叠。
- 清除浮动:BFC可以包裹浮动元素,使其不对其他元素造成影响。通过创建一个新的BFC,可以清除浮动带来的高度塌陷问题。
- 阻止浮动元素重叠:在BFC中,浮动元素会受到BFC边界的限制,从而阻止其与其他元素的重叠。这有助于解决一些浮动布局导致的错位或溢出问题。
- 自适应两栏布局:BFC可以使用浮动或者
overflow: hidden;
等方式创建,结合盒模型和清除浮动,可以实现一些常见的布局需求,如自适应两栏布局。
创建BFC的方式有多种,包括:
- 给元素添加
float
属性。 - 给元素添加
display: inline-block;
、display: table-cell;
、display: table-caption;
等属性。 - 给元素添加
overflow
属性为除visible、clip
之外的值。
总而言之,BFC是一种控制元素布局行为的机制,它可以解决一些常见的布局问题,如外边距塌陷、浮动元素重叠等。了解BFC的使用和原理对于前端开发中复杂布局的实现非常有帮助。
⭐实现水平居中
-
行内元素:text-align:center
-
块级元素:
- margin: 0 auto(确定宽度的情况下)
- transform + 绝对定位
- display:flex + justify-content:center
- 子display:inline-block + 父text-align:center
- display:table + margin:auto
⭐实现垂直居中
- 对于文字:设置line-height
- flex + align-items
- transform + 定位
- display:table + verticle-align:middle
⭐水平垂直居中
① 使用Flexbox:通过将父容器设置为display: flex;
,然后使用justify-content: center;
和align-items: center;
属性来水平和垂直居中子元素。
css
.parent {
display: flex;
justify-content: center;
align-items: center;
}
② 使用Grid布局:通过将父容器设置为display: grid;
,然后使用place-items: center;
属性来水平和垂直居中子元素。
css
.parent {
display: grid;
place-items: center;
}
③ 使用绝对定位和transform属性:将子元素的位置设置为绝对定位,并且使用top: 50%;
和left: 50%;
将元素的左上角移动到父容器的中心,然后使用transform: translate(-50%, -50%);
将元素的中心与父容器的中心对齐。
css
.parent {
position: relative;
}
.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
④ 使用表格布局:将父容器设置为display: table;
,然后将子元素设置为display: table-cell;
和vertical-align: middle;
以实现水平和垂直居中。(文本水平垂直居中)
css
.parent {
display: table;
}
.child {
display: table-cell;
vertical-align: middle;
text-align: center;
}
⑤父flex + 子margin:auto
⑥绝对定位均设0 + margin:auto
⭐圣杯布局&双飞翼布局
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
/*
思路:
1、容器宽度设为100%来填充整个页面宽度
2、为左右中三者设置float:left,以此可以显示在一行
3、为left和right部分设置负的margin值,让它们移到center行中,但注意,现在的left和right将center左右部分覆盖了
4、解决覆盖问题,有两种方式:
(1) 为center左右设置内边距,但这时会将页面撑大,所以我们要将内边距合并到100%的宽度里,这里我们想到了用怪异盒模型
(2) 用定位的方式解决,为容器wrapper设置内边距,然后为left和right设置相对定位,并为其移动到左右两边
扩展:
与双飞翼布局的区别?
圣杯布局通过设置负边距与容器内边距来使三栏显示于一列,而双飞翼布局则为中间列额外包裹添加外边距来防止左右两侧遮挡,两者实现效果相似
*/
.item {
height: 100px;
/* 这里设置浮动是为了让三者可以并排显示 */
float: left;
}
.center {
/* 为了防止左右两块与center重叠,我们要为其设置内边距 */
/* padding: 0 300px; */
/* 方式一:将盒子模型设为怪异盒模型,防止padding将左右顶开 */
/* box-sizing: border-box; */
}
.center {
width: 100%;
background-color: green;
}
.wrap {
padding:0 300px;
}
.left {
width: 300px;
background-color: yellow;
/* 为了让left块和center在同一行,需要用负的margin值让它向回移动 */
margin-left: -100%;
/* 方式二:设relative */
position: relative;
left: -300px;
}
.right {
width: 300px;
background-color: red;
/* 为了让right块和center在同一行,需要用负的margin值让它向回移动 */
margin-left: -300px;
/* 方式二:设relative */
position: relative;
right: -300px;
}
</style>
</head>
<body>
<!-- 圣杯布局分左右中三部分 -->
<div class="wrap">
<!-- 为了让中间内容先渲染,写在上面 -->
<div class="item center"></div>
<div class="item left"></div>
<div class="item right"></div>
</div>
</body>
</html>
⭐clearfix
css
.clearfix::after {
content: "";
clear: both;
display: block;
}
clearfix通过为父元素添加伪元素,并为其添加清除浮动效果来解决浮动元素引起的高度塌陷问题,这里设置display:table主要有两方面作用:
- 生成独立的BFC,从而避免浮动元素对其周围其他元素的影响
- 表格布局有自动调整单元格高度的特性,即表格单元格会根据内容自动撑开高度。通过将伪元素设置为
display: table
,可以使其撑开父元素的高度,从而让父元素正确地包含浮动元素。
⭐flex
对于轴线:
-
设定主轴方向
-
flex-direction:
- row(default)
- row-reverse
- column
- column-reverse
-
-
是否换行
-
flex-wrap:
- nowrap(default)
- wrap
- wrap-reverse
-
-
前两者可以合并
-
flex-flow:
- row nowrap(default)
-
-
主轴对齐方式
-
justify-content:
- flex-start(default) 左对齐
- flex-end 右对齐
- center
- space-between 两端对齐,中间间隔相同
- space-around 每个成员两侧的间隔相等
- space-evenly 均匀分布,两侧靠边间隔与元素之间间隔也相同
-
-
成员在交叉轴(主轴外的另一个轴)的对齐方式,默认可以理解为y轴
-
align-items:
- flex-start 交叉轴起点对齐
- flex-end 交叉轴终点对齐
- center
- baseline 文字基线对齐
- stretch(default) 若未设置高度,则默认占满整个交叉轴
-
-
多根轴线(多行)的对齐方式(多根主轴在交叉轴上的对齐方式)
-
align-content:
- flex-start 与交叉轴起点对齐
- flex-end 与交叉轴终点对齐
- center
- space-between 交叉轴两端对齐,中间平均分布
- space-around 每根轴线两侧间隔都相同
- space-evenly 均匀分布,两侧靠边间隔与元素之间间隔也相同
- stretch(default) 占满整个交叉轴
-
对于其中的每一项:
-
order 值为数字,为排序的权重
-
flex-grow 值为数字,意为成员的放大比例,默认为0,若都设为1,则会放大等分剩余空间(忽略其原本宽度,全部等分)
-
flex-shrink 值为数字,意为成员的缩小比例,默认为1,若空间不足,则会缩小相同比例,若设为0,则不缩小,若都不缩小,则会超出
-
flex-basis 值为长度,意为在未分配剩余空间时,成员占据的主轴空间大小,默认为auto,即为成员的本来大小
-
flex 为flex-grow flex-shrink flex-basis的简写,默认为0 1 auto ,后两者可选,我们常用这个属性,其实本质大多数用的都是flex-grow
-
align-self 单个成员的交叉轴对齐方式
- auto(default) 继承自父元素 如果没有父元素,则默认为stretch
- flex-start
- flex-end
- center
- baseline
- stretch
⭐清除浮动
浮动的原理是让图片脱离文档流,直接浮在桌面上。我们一般布局的时候都是只设置宽度不设置高度,让内容来自动填充高度。但使用浮动后会让原本填充的高度消失,父元素高度为0,后续添加内容布局会产生混乱,造成高度塌陷,这时候就可以利用清除浮动来解决父元素高度塌陷的问题。
浮动导致的问题及其解决方案:
-
父元素高度塌陷
-
父元素固定宽高(适用于宽高确定的板块)
-
在浮动元素后面新加一个空标签,并设置clear: both;
- 优点:简单
- 缺点:添加了许多空的div,语义化差
-
overflow: hidden
- 优点:简单易用,浏览器支持好
- 缺点:要设置width和zoom,且有时会裁剪掉部分元素
-
clearfix::after(主流做法)
css.clearfix:after{/*伪元素是行内元素 正常浏览器清除浮动方法*/ content: ""; display: block; height: 0; clear:both; visibility: hidden; } .clearfix{ *zoom: 1;/*ie6清除浮动的方式 *号只有IE6-IE7执行,其他浏览器不执行*/ }
-
-
同级非浮动元素被覆盖
- 给被遮盖的元素也设置浮动
- 为被遮盖的元素触发BFC(宽度足够会同行显示)
- 为被遮盖的元素设置clear: both;(会换行显示)
⭐Grid布局
①定义grid布局:
- display: grid/inline-grid
②设置行与列的布局
-
grid-template-rows / grid-template-columns
-
基本使用:grid-template-rows(grid-template-columns): 100px 100px 200px,设置三行(列),高度(宽度)分别为100 100 200px
-
repeat():grid-template-rows(grid-template-columns): repeat(3, 100px),设置三行(列),高度(宽度)均为100px
- auto-fill:grid-template-rows(grid-template-columns): repeat(auto-fill, 100px),不定有几行/几列,能装多少装多少,当可用空间不足以容纳更多的列时,会创建空的隐式网格项来填充空白区域
- auto-fit:尽量将每一列放入网格容器中,使得不留下空白的空间,当可用空间不足以容纳更多的列时,会自动调整已有的网格项的大小,以填充剩余空白区域
-
fr:占比,与flex-grow类似,grid-template-rows(grid-template-columns): 100px 1fr 2fr,除去100px,剩下按1:2划分
-
minmax():grid-template-rows(grid-template-columns): 1fr 1fr minmax(100px, 2fr),表示第三行(列)最少为100px,最大不能超过前两行(列)的两倍
-
auto:自动调整,grid-template-rows(grid-template-columns): 100px auto 100px,实现自由适配的三栏布局
-
-
row-gap / columns-gap / gap:行间距列间距以及二合一写法
③划分区域自定义放置元素
-
grid-template-areas:划分区域
cssgrid-template-columns: 120px 120px 120px; grid-template-rows: repeat(3, 300px); grid-template-areas: ". header header" "sidebar content content" "sidebar . .";
定义三行三列,通过grid-template-areas来划分区块,并为其命名, . 表示空
-
grid-area:放置元素
css.sidebar { grid-area: sidebar; } .content { grid-area: content; } .header { grid-area: header; }
规定哪个类的内容放在哪个区块中
④划分区域自动填充元素
-
grid-auto-flow
- row 默认值,按行进行依次填充
- column,按列进行依次填充
- 附加值dense,是否紧凑填充,可以自动在后方找到合适大小的块填充到前面的单元格中,防止按序填充时出现元素过大填充不满的现象
⑤自由安排元素位置
-
grid-column-start、grid-column-end、grid-row-start、grid-row-end:分别对应网格的上边框位置、下边框位置、左边框位置、右边框位置,通过这四个元素将内容定位到网格中(左上方顶点为1,向右向下递增)
- span:声明项目跨度(占几个格子),在start中设置,end就可以不用设置
-
简写形式:grid-area: 2 / 1 / 4 / 3(其中,2表示起始行,1表示起始列,4表示结束行,3表示结束列)
⑥所有元素在单元格内的对齐方式
-
justify-items / align-items:水平/垂直对齐方式
- start
- end
- center
- stretch(默认)
-
place-items:水平和垂直的合并形式,写两个就是左水平右垂直,写一个就是两者共用
⑦单个元素在单元格内的对齐方式
- justify-self、align-self、place-self:与⑥中用法完全一致,只是针对单个元素使用
⑧单元格在容器内的对齐方式
-
justify-content / align-content:水平/垂直对齐方式
- start(默认)
- end
- center
- stretch
- space-between
- space-around
- space-evenly
-
place-content为上两者的合并形式
⑨多出来的东西怎么放?隐式网格
- grid-auto-columns / grid-auto-rows:定义隐式网格的单元格长宽
一些面试题
- 请解释什么是CSS Grid布局?
- Grid布局中有哪些主要的概念和术语?
- 请解释Grid容器和Grid项之间的区别是什么?
- 如何在父元素上应用Grid布局?
- 请解释Grid布局中的行和列是如何定义的?
- 如何指定Grid项在网格中的位置?
- Grid布局中的重叠和层叠是如何处理的?
- 如何控制Grid项的大小和对齐方式?
- 请解释Grid布局的自动布局机制。
- 如何媒体查询和响应式设计与Grid布局结合使用?
⭐选择器
基本选择器:id class 标签 层次选择器:后代、子代、兄弟、通用 结构选择器:第一个、最后一个、奇数偶数个、伪类、伪元素 属性选择器:属性
⭐animation有哪些属性
CSS 的 animation
属性用于为元素添加动画效果。以下是 animation
属性的一些常用子属性:
1、animation-name
: 定义动画的名称。
css
.animation {
animation-name: myAnimation;
}
@keyframes myAnimation {
0% { opacity: 0; }
100% { opacity: 1; }
}
2、animation-duration
: 定义动画的持续时间。
css
.animation {
animation-name: myAnimation;
animation-duration: 2s;
}
3、animation-timing-function
: 定义动画的时间函数(即动画进度的加速度)。
css
.animation {
animation-name: myAnimation;
animation-timing-function: ease-in-out;
}
4、animation-delay
: 定义动画开始之前的延迟时间。
css
.animation {
animation-name: myAnimation;
animation-delay: 1s;
}
5、animation-iteration-count
: 定义动画的播放次数,infinite为永不停止。
css
.animation {
animation-name: myAnimation;
animation-iteration-count: 3;
}
6、animation-direction`: 定义动画的播放方向。
css
.animation {
animation-name: myAnimation;
animation-direction: reverse;
}
7、animation-fill-mode
: 定义动画在播放之前和之后如何应用样式。
none
:默认值,动画不会对元素应用任何样式。在非活动时间段,元素将恢复到初始样式。forwards
:在动画结束后,元素将保持动画结束状态。也就是说,元素将保持在最后一帧的状态。backwards
:在动画开始前,元素将立即应用动画的第一帧样式。这意味着动画开始前的状态会提前被应用。both
:结合了forwards
和backwards
,在动画开始前和结束后都会应用样式。
css
.animation {
animation-name: myAnimation;
animation-fill-mode: forwards;
}
8、animation-play-state
: 定义动画的播放状态。
css
.animation {
animation-name: myAnimation;
animation-play-state: paused;
}
这些是 animation
属性的一些常用子属性,它们可以用来控制动画的各个方面,从而创建出丰富多样的动画效果。使用这些属性可以自定义动画的外观、持续时间、延迟等。要实现动画效果,还需要定义动画的关键帧(@keyframes
)规则,包含动画在不同百分比时的样式。
在 CSS 中,可以使用 @keyframes
规则来定义动画的关键帧(即动画的不同阶段)。@keyframes
规则定义了动画的每个关键帧以及相应的样式。
下面是一个使用 @keyframes
定义动画关键帧的示例:
css
@keyframes slide {
0% {
transform: translateX(0);
}
50% {
transform: translateX(100px);
}
100% {
transform: translateX(200px);
}
}
在这个例子中,我们创建了一个名为 slide
的动画。通过 @keyframes
规则,我们定义了三个关键帧:0%、50% 和 100%。
- 在 0% 关键帧,元素的
transform
属性设置为translateX(0)
,即初始状态。 - 在 50% 关键帧,元素的
transform
属性设置为translateX(100px)
,表示动画进行到一半时的状态。 - 在 100% 关键帧,元素的
transform
属性设置为translateX(200px)
,表示动画结束时的状态。
你可以在关键帧中定义任意数量的状态,并根据需要设置不同的样式属性。使用百分比来表示关键帧的时间点,动画会根据关键帧之间的插值进行平滑过渡。
定义完关键帧后,你可以使用 animation-name
属性将动画应用到具体的元素上,如下所示:
css
.element {
animation-name: slide;
animation-duration: 2s;
}
通过将 animation-name
设置为关键帧名称,你可以将定义好的动画应用于元素,并设置动画的持续时间(在此示例中为 2 秒)。
请注意,在实际使用中,除了设置关键帧的样式,你还可以使用其他属性调整动画的速度、延迟、重复次数等。根据具体需求,你可以进一步完善动画效果。
⭐说说transition
CSS 的 transition
属性用于在元素发生状态变化时,平滑地过渡(或动画)到新的样式。它允许你控制一个或多个 CSS 属性的变化过程,使得元素的变化更加柔和和可控。
transition
属性由以下几个子属性组成:
1、transition-property
:指定要过渡的 CSS 属性名称,可以是单个属性,也可以是多个属性。 当 transition
属性只设置了 transition-duration
值时,没有指定 transition-property
值,默认情况下所有可过渡的 CSS 属性都会应用过渡效果。
css
.element {
transition-property: width;
}
2、transition-duration
:指定过渡的持续时间,以秒(s)或毫秒(ms)为单位。
css
.element {
transition-duration: 0.5s;
}
3、transition-timing-function
:指定过渡的时间函数,用于控制过渡的速度曲线。常见的值包括 ease
(默认值)、linear
、ease-in
、ease-out
和 ease-in-out
,也可以使用贝塞尔曲线来定义自定义的速度曲线。
css
.element {
transition-timing-function: ease-in-out;
}
4、transition-delay
:指定过渡开始之前的延迟时间,以秒(s)或毫秒(ms)为单位。
css
.element {
transition-delay: 0.2s;
}
通过使用这些子属性,你可以控制元素的过渡效果,从而实现例如悬停时颜色渐变、尺寸变化、透明度渐变等效果。
例如,下面的代码演示了一个当鼠标悬停在元素上时,背景颜色发生渐变过渡的效果:
css
.element {
background-color: blue;
transition-property: background-color;
transition-duration: 0.5s;
transition-timing-function: ease-in-out;
}
.element:hover for(let [key, value] of map) {
if(largeStr.indexOf(key)!==-1) {
}
}
background-color: red;
}
以上代码将使元素的背景颜色在悬停时从蓝色平滑地过渡到红色,过渡时间为 0.5 秒,速度曲线为先加速后减速。这只是 transition
属性的一个简单示例,你可以根据需要调整和组合这些子属性来实现更复杂的过渡效果。
⭐外边距塌陷解决方案
外边距塌陷是指在垂直方向上,两个相邻元素的外边距(margin)合并成一个外边距的现象。以下是几种常见的外边距塌陷情况及其解决方案:
-
父子元素外边距塌陷:
- 情况:父元素的上边距与第一个子元素的上边距合并,导致外边距塌陷。
- 解决方案:给父元素添加
overflow: hidden;
或float: left/right;
属性,为父元素创建新的块级格式化上下文(BFC),从而阻止外边距的合并。
-
相邻兄弟元素外边距塌陷:
- 情况:两个相邻的兄弟元素,上一个元素的下边距与下一个元素的上边距合并,导致外边距塌陷。
- 解决方案:给其中一个元素添加
padding-top
或border-top
属性,或者在两个元素之间插入空元素(例如<div></div>
)。
-
空元素与外边距塌陷:
- 情况:一个没有内容的空元素或没有上边框/上内边距的元素,上边距与其下面的元素的上边距合并,导致外边距塌陷。
- 解决方案:给空元素添加
padding-top
或border-top
属性,或者为其添加display: inline-block;
属性。
-
内部包含块的外边距塌陷:
- 情况:一个元素的内部包含块(例如
<div>
)中的第一个子元素的上边距与外部元素的上边距合并,导致外边距塌陷。 - 解决方案:为外部元素添加
overflow: hidden;
或float: left/right;
属性,将其变为 BFC,阻止外边距合并。
- 情况:一个元素的内部包含块(例如
这些解决方案的原理都是通过改变元素的布局特性来创建新的块级格式化上下文(BFC),从而阻止外边距的合并。BFC 可以独立地控制元素的布局行为,使得外边距不再发生塌陷。
⭐说一说overflow的各个值
- visible:正常显示,多余部分超出盒子显示
- scroll:默认显示滚动条
- hidden:不提供滚动条,也不支持用户滚动,多于内容会被裁剪,但可以通过脚本控制滚动
- clip:和hidden类似,但不允许通过脚本滚动,也不会生成BFC
- auto:溢出显示滚动条,不溢出则不显示,生成BFC
- overlay:行为与auto相同,但滚动条绘制在内容之上,不占据空间
⭐说说伪类和伪元素
伪类: 用于已有元素处于某种状态时为其添加对应的样式,这个状态是根据用户行为而动态变化的
例如:当用户悬停在指定元素时,可以通过:hover来描述这个元素的状态,虽然它和一般css相似,可以为 已有元素添加样式,但是它只有处于DOM树无法描述的状态下才能为元素添加样式,所以称为伪类
伪元素: 用于创建一些不在DOM树中的元素,并为其添加样式
例如,我们可以通过:before来在一个元素之前添加一些文本,并为这些文本添加样式,虽然用户可以看见 这些文本,但是它实际上并不在DOM文档中
⭐引入样式时,link和@import的区别?
- 加载顺序:
<link>
在页面加载时同时被加载,而@import
在页面加载后才被加载。因此,<link>
可以更快地加载外部资源,并且不会阻塞页面的加载,而@import
会延迟页面的显示,直到所有的 CSS 文件加载完毕。 - 兼容性:
<link>
是 HTML 标签,可以被所有浏览器和搜索引擎正确解析。而@import
是 CSS 的语法规则,只能被符合 CSS3 规范的浏览器正确解析,低版本的浏览器可能会忽略@import
声明。 - 使用方式:
<link>
可以用于加载任何类型的文件,如 CSS、JavaScript、图像、音频等。而@import
只能用于加载 CSS 文件。 - 作用域:
<link>
中的样式表对整个页面都起作用,而@import
只对当前样式表中的规则起作用。
综上所述,<link>
更适合引入外部资源,而 @import
更适合在 CSS 文件中嵌入其他 CSS 文件。如果需要同时加载多个 CSS 文件,最好使用 <link>
,以提高页面的性能和可访问性。
⭐display、visibility、opacity的区别
- display:none; DOM结构:浏览器不会渲染display属性为none的元素,不占据空间,意思就是页面上没有它的一席之地,你在开发者模式中选不中那个元素。 事件监听:无法进行DOM事件监听。 性能:动态改变此属性时会引起回流,性能较差。 继承:不会被子元素继承,因为子元素也不被渲染。 transtion过渡不支持display。
- visibility:hidden; DOM结构:元素被隐藏了,浏览器会渲染visibility属性为hidden的元素,占据空间,意思就是页面上有它的空间,在开发者模式中能选中那个元素。 事件监听:无法进行DOM事件监听。 性能:动态改变此属性时会引起重绘,性能较高。 继承:会被子元素继承,子元素通过设置visibility:visible;来显示自身,使子元素取消自身隐藏。 transtion:visibility会立即显示,隐藏时会延时。
- opacity:0; DOM结构:opacity属性值为0时透明度为100%,元素隐藏,占据空间,opacity值为0到1,为1时显示元素。 事件监听:可以进行DOM事件监听。 性能:提升为合成层,不会引发其他元素的重绘,性能较高。 继承:会被子元素继承,子元素不能通过设置opacity:1;来取消隐藏。 transtion:opacity可以延时显示与隐藏。
🌙JS
⭐简述JavaScript对象、函数、数组的定义。
对象(Object):JavaScript 的对象是一种复合数据类型,用于存储键值对。对象由一组属性(properties)组成,每个属性都有一个键(key)和对应的值(value)。对象可以通过字面量表示法或构造函数来创建。
函数(Function):JavaScript 是一门函数式编程语言,函数在 JavaScript 中是一等公民。函数是可执行的代码块,可以接收参数并返回一个值。可以通过函数声明或函数表达式的方式定义函数。
数组(Array):JavaScript 数组是一种有序、可变的数据集合,可以存储多个值。数组可以包含不同类型的数据,并且长度是动态的。可以使用字面量表示法或构造函数来创建数组。
⭐说说JS中的隐式转换
隐式类型转换是指 JavaScript 在比较或计算操作中自动将一个数据类型转换为另一个数据类型。这种类型转换是根据 JavaScript 的类型转换规则进行的,以下是一些常见的隐式类型转换规则:
==
- 字符串和数字之间的比较:如果一个操作数是字符串,另一个操作数是数字,JavaScript 会将字符串转换为数字,然后进行比较。
- 布尔值和其他类型之间的比较:布尔值在进行比较时,会被转换为数字,
true
被转换为1
,false
被转换为0
。 - 对象和原始值之间的比较:对象在进行比较时,JavaScript会先使用
valueOf()
方法获取对象的原始值,然后再根据前面提到的方法将两个值进行比较。如果valueOf()
方法无法返回原始值,则会尝试调用toString()
方法来获取值。 null
和undefined
的比较:它们在进行相等比较时被认为是相等的。
运算
- string+其他基本数据类型,转为string
- 其他基本数据类型与的number类型操作,均转为number
补充问题:
当a为多少时,if判断成立
css
let a
if(a == 1&&a == 2&&a == 3) {
console.log('success!')
}
答案:
css
a = {
a:1,
valueOf() {
return a++
}
}
⭐前端性能优化
首屏优化
- 压缩、分包(利用浏览器并行的能力加载)、删除无用代码
- 静态资源分离,如放在CDN上面
- JS脚本非阻塞加载(或将其放在最底部,最后加载,以防阻塞页面渲染)
- 合理利用缓存
- 服务器端渲染(SSR)
- 预置loading、骨架屏
渲染优化
-
GPU加速,利用GPU来处理复合图层(像素密集型)进行"加速"
-
减少回流、重绘,例如:
- 避免使用CSS表达式,因为当页面变化时,都会进行重新计算
- 不适用table布局,因为该布局中的一个小改动都会引起整个table重新布局,所以现在table布局已经几乎淘汰
- 能使用css就不使用js
-
离屏渲染,正常情况下数据经过计算和渲染后,就会直接显示在屏幕上,而使用离屏渲染,就会在内存将画面全部渲染好,再放在页面上,以防画面过于复杂而使用户感到掉帧
-
懒加载,将需要的资源提前加载到本地,需要时直接从缓存中取出,节约时间。
JS优化
-
防止内存泄露,比如:
- 使用全局变量不当产生的内存泄露
- DOM引用没有及时清除,导致DOM删除后但JS引用依旧存在,占用资源
- 定时器没有及时关闭(所以建议自己封装可以自动关闭的定时器组件)
-
循环语句尽早break
-
合理使用闭包
-
减少DOM访问,因为JS引擎和渲染引擎交互较为费时,如果需要常用,要使用一个变量将其缓存起来,不要每次都去查询获取
-
防抖------多次点击,只执行最后一次
-
节流------一定时间内,多次调用只执行一次
-
Web Worker(工作线程),可以将一些耗时的数据处理操作从主线程中剥离出来,使主线程更加专注于页面渲染和交互。
⭐跨浏览器兼容问题
- CSS盒模型差异,可以使用CSS重置或使用标准化库来进行统一各浏览器的默认样式
- JavaScript API兼容性问题,可以通过特性检测来判断当前浏览器是否支持某个API,或使用polyfill库来提供缺失的功能
- flex布局兼容性问题,可以使用grid等布局来作为替代方案
⭐如何保证代码的质量和可维护性
- 模块化,特定的模块负责特定的功能,提高代码的可复用性,降低耦合
- 通过一些设计模式来管理实例,例如单例模式、观察者模式等
- 进行一些单元测试
- 代码复用,提出可复用的函数及组件
- 错误处理,合理处理异常和错误情况
- 使用代码质量检查工具来检查修复代码规范问题,例如ESLint
⭐关于变量提升和函数提升的问题
r
var c = 1;
function c(c) {
console.log(c)
}
c(2)
详见红宝书P26
css
//函数作⽤域:局部作⽤域
var a = 1;
function b() {
a = 10;
return;
//a函数声明,提前变量a,将a认为是函数b作⽤域的变量,具有局部效果
function a(){}
}b();
console.log(a); // 1
ini
var m= 1, j = k = 0;
function add(n) {
return n = n+1;
}
y = add(m);
function add(n) {
return n = n + 3;
}
z = add(m);
⭐说一下Vue2响应式的实现
- 创建 Vue 实例时,会调用
new Vue()
,其中会通过observe
函数对data
数据进行观测。 - 在
observe
函数中,会使用Object.defineProperty
递归地遍历所有属性,并为每个属性创建对应的getter
和setter
。 - 在属性的
getter
中,会进行依赖收集。首先,通过闭包引用一个全局变量Dep.target
,该变量保存当前正在计算的 Watcher 实例。然后,在getter
中调用dep.depend()
方法,将当前的 Watcher 添加到依赖集合中。 - 在属性的
setter
中,当属性的值发生改变时,会触发setter
。在setter
中,会对新值进行响应式处理(递归调用observe
),并通知相关的依赖进行更新。这一过程通过调用dep.notify()
实现,dep.notify()
会遍历依赖集合,调用每个依赖的update
方法,触发相应的更新操作。 - 在模板编译过程中,解析模板中的指令表达式,对每个指令表达式生成对应的 Watcher 实例。Watcher 实例内部会调用属性的
getter
方法,在getter
中触发依赖收集的过程,将当前 Watcher 添加到依赖集合中。 - 当属性的值发生变化时,会触发相应的更新操作。依赖收集过程中收集到的 Watcher 实例会在更新阶段被遍历,并调用其
update
方法来更新视图。
⭐关于跨域
跨域请求是指在浏览器中,当请求的源地址与请求地址的协议、域名、端口号不一致时,会被浏览器的同源策略所限制,这是为了保护用户的信息安全。
- JSONP(JSON with Padding):JSONP通过动态创建
- CORS(Cross-Origin Resource Sharing):CORS是一种现代的跨域解决方案,通过在服务器端设置响应头部,允许客户端跨域访问资源。在前端开发中,可以通过设置服务器的响应头部字段Access-Control-Allow-Origin来允许特定的源进行跨域请求。
- 代理服务器(Proxy Server):通过在自己的服务器上设置一个代理服务器,将跨域请求转发到目标服务器,并将目标服务器的响应返回给前端。这种方法需要对服务器进行配置,适用于无法直接修改目标服务器响应头部的情况。
- WebSocket:WebSocket是一种全双工通信协议,可以在同一域名下使用不同的端口进行跨域通信。通过建立WebSocket连接,可以实现实时通信和跨域数据传输。
⭐请解释一下什么是响应式设计(Responsive Design),以及在开发响应式网站时需要考虑哪些方面,并列举常用方法
响应式设计(Responsive Design)是一种网页设计和开发的方法,旨在使网站能够适应不同设备、屏幕尺寸和浏览器窗口大小,以提供更好的用户体验。响应式设计通过使用灵活的布局、弹性的图像和媒体查询等技术,让网页能够根据用户设备的特性进行自适应和优化。
在开发响应式网站时,需要考虑以下几个方面:
- 弹性布局:使用相对单位(如百分比、em、rem)而不是固定像素来定义元素的尺寸和布局。这样可以让网页的元素根据屏幕尺寸的变化而自动调整大小和位置。
- 媒体查询:使用CSS的媒体查询功能来检测设备的特性(如屏幕宽度、触摸支持等),并根据不同的条件应用不同的样式规则。这样可以根据不同设备的屏幕大小和特性来调整网页的布局和样式。
- 图片优化:针对不同设备加载适当尺寸和分辨率的图片,以减少加载时间和带宽占用。可以使用响应式图片技术,如
<img srcset>
和<picture>
元素来提供多个不同尺寸的图片,并根据设备的屏幕密度和大小选择合适的图片加载。 - 触摸友好性:优化网页在触摸设备上的交互和操作体验,比如增大可点击区域、使用合适的手势和触摸事件等。
- 导航菜单:设计合适的导航菜单,以便在小屏幕设备上能够轻松导航和访问各个页面。
- 测试和调试:在不同设备、不同浏览器和不同分辨率下进行测试和调试,以确保网页在各种情况下都能正常显示和运行。
- 性能优化:考虑网页加载速度和性能,减少不必要的资源请求和加载,使用压缩和缓存等技术来提高网页的响应速度。
以下是一些常用的技术和方法来实现响应式设计:
- 媒体查询(Media Queries):使用CSS的媒体查询功能,根据设备的特性(如屏幕宽度、设备类型等)应用不同的样式。通过设置不同的CSS规则,可以根据设备的宽度、高度、方向等特性来改变布局和样式。
- 相对单位(例如em和rem):相对单位可以根据父元素或根元素的大小进行调整。em单位相对于父元素的字体大小,rem单位相对于根元素(通常是html元素)的字体大小。使用这些相对单位可以根据设备的屏幕大小自适应调整元素的大小。
- 百分比布局:使用百分比作为元素的宽度、高度等属性值,以便根据容器的大小来调整元素的尺寸。
- 视口单位(例如vw、vh):视口单位是相对于视口宽度和高度的单位。vw表示视口宽度的百分比,vh表示视口高度的百分比。使用视口单位可以根据屏幕的实际尺寸来调整元素的大小。
- 弹性盒布局(Flexbox):弹性盒布局是一种灵活的布局方式,可以使容器中的元素在不同尺寸的屏幕上自动调整位置和大小。
- 栅格布局(Grid):网格布局是一种二维布局系统,可以将页面划分为行和列,并指定元素在网格中的位置。通过设置不同的网格模板和位置属性,可以实现灵活的响应式布局。
- 响应式图片:使用srcset和sizes属性来提供不同尺寸的图片,以适应不同设备的显示需求。
- 断点(Breakpoints):根据不同设备的屏幕宽度和布局需求,设置断点来改变页面的布局和样式。
⭐什么是单页面应用(SPA)?它与传统多页面应用相比有什么优势和劣势?
单页面应用(Single Page Application,SPA)是一种Web应用程序的架构模式,它在加载初始HTML页面后,通过JavaScript动态地更新页面的内容,而不需要每次页面切换时重新加载整个页面。
与传统的多页面应用相比,SPA有以下优势:
- 更好的用户体验:由于SPA使用了异步加载和局部更新的方式,用户在浏览网站时可以享受到更流畅、快速的交互体验,减少了页面刷新的延迟。
- 较少的网络请求:SPA通常在初始加载时会下载所有所需的JavaScript、CSS和其他静态资源,之后只需要通过API请求数据。相比多页面应用,SPA减少了对服务器的频繁请求,提高了性能。
- 良好的代码组织:SPA使用组件化的方式组织代码,将页面拆分为多个可重用的组件。这样可以提高代码的复用性、可维护性和可测试性,使开发工作更加高效。
然而,SPA也存在一些劣势:
- 初始加载较慢:由于SPA需要在初始加载时下载所有所需资源,首次加载时间可能会较长。这对于一些网络条件较差的用户来说可能是一个问题。
- 对SEO不友好:传统的搜索引擎爬虫通常会根据HTML页面的内容进行索引,而SPA的内容是通过JavaScript动态生成的,对搜索引擎的爬取和索引不太友好。虽然有一些技术可以解决这个问题(如服务器端渲染),但相对复杂。
- 依赖于JavaScript:由于SPA大量依赖于JavaScript来处理页面逻辑和数据交互,因此如果用户禁用了JavaScript,SPA可能无法正常运行。
⭐JS垃圾回收机制
-
项目中,如果存在大量不被释放的内存(堆/栈/上下文),页面性能会变得很慢。当某些代码操作不能被合理释放,就会造成内存泄漏。我们尽可能减少使用闭包,因为它会消耗内存。
-
浏览器垃圾回收机制/内存回收机制:
浏览器的
Javascript
具有自动垃圾回收机制(GC:Garbage Collecation
),垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。标记清除 :在
js
中,最常用的垃圾回收机制是标记清除:浏览器会为内存中所有的变量添加标记,然后再遍历上下文,为其中所有变量及被变量引用的变量清除标记,这样剩下被标记的就是待回收对象。 谷歌浏览器 :"查找引用",浏览器不定时去查找当前内存的引用,如果没有被占用了,浏览器会回收它;如果被占用,就不能回收。 IE浏览器:"引用计数法",当前内存被占用一次,计数累加1次,移除占用就减1,减到0时,浏览器就回收它。 -
优化手段:内存优化 ; 手动释放:取消内存的占用即可。
(1)堆内存:fn = null 【null:空指针对象】
(2)栈内存:把上下文中,被外部占用的堆的占用取消即可。
(3)防止出现隐藏类,在一开始调用构造函数生成实例时就传入需要的变量,防止动态添加和删除属性
(4)静态分配和对象池
-
内存泄漏
在 JS 中,常见的内存泄露主要有 4 种,全局变量、闭包、DOM 元素的引用、定时器
⭐JS 中 this 的五种情况
- 作为普通函数执行时,
this
指向window
。 - 当函数作为对象的方法被调用时,
this
就会指向该对象
。 - 构造器调用,
this
指向返回的这个对象。 - 箭头函数捕获的是最近一层非箭头函数作用域的
this
值,不会绑定到字面量复制的对象上。 - 基于Function.prototype上的
apply 、 call 和 bind
调用模式,这三个方法都可以显示的指定调用函数的 this 指向。apply
接收参数的是数组,call
接受参数列表,bind
方法通过传入一个对象,返回一个this
绑定了传入对象的新函数。这个函数的this
指向除了使用new
时会被改变,其他情况下都不会改变。若为空默认是指向全局对象window。
⭐原型 && 原型链
原型关系:
- 每个 class都有显示原型 prototype
- 每个实例都有隐式原型 __ proto__
- 实例的_ proto_指向对应 class 的 prototype
原型: 在 JS 中,每当定义一个对象(函数也是对象)时,对象中都会包含一些预定义的属性。其中每个函数对象
都有一个prototype
属性,这个属性指向函数的原型对象
。
原型链 :函数的原型对象上的constructor默认指向函数本身,原型对象除了有原型属性外,为了实现继承,还有一个原型链指针_proto_ ,该指针是指向上一层的原型对象,而上一层的原型对象的结构依然类似。因此可以利用_proto_ 一直指向Object的原型对象上,而Object原型对象用Object.prototype.__ proto__ = null表示原型链顶端。如此形成了js的原型链继承。同时所有的js对象都有Object的基本方法
特点: JavaScript
对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变。
⭐new运算符的实现机制
- 首先创建了一个新的
空对象
设置原型
,将对象的原型设置为函数的prototype
对象。- 让函数的
this
指向这个对象,执行构造函数的代码(为这个新对象添加属性) - 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
⭐请解释一下什么是事件委托(或称事件代理),以及它在前端开发中的作用是什么?
事件委托(或称事件代理)是一种在前端开发中常用的技术,它利用了事件冒泡机制,将事件处理程序绑定到一个父元素上,而不是直接绑定到每个子元素上。当事件触发时,事件会经过冒泡阶段依次向上传播至父元素,父元素可以根据事件的目标元素来判断应该执行哪个事件处理函数。
事件委托的作用主要有以下几点:
- 减少事件处理程序的数量:通过将事件处理程序绑定到父元素上,可以避免为每个子元素都绑定事件处理程序,减少内存消耗。
- 动态绑定事件:对于通过 JavaScript 动态添加的子元素,使用事件委托可以自动为它们绑定事件处理程序,无需额外的操作。
- 简化代码结构:使用事件委托可以将事件处理程序集中管理,使代码更加简洁、可读性更高。
举个例子,假设有一个列表的父元素,里面包含了多个子元素(项),我们需要为每个子元素绑定点击事件,实现点击子元素时改变其样式。使用事件委托可以这样处理:
csharp
// 获取父元素
const parentElement = document.getElementById('parent');
// 绑定点击事件到父元素
parentElement.addEventListener('click', function(event) {
// 判断事件目标是否是子元素(项)
if (event.target && event.target.nodeName === 'LI') {
// 执行事件处理逻辑,例如改变样式
event.target.classList.toggle('active');
}
});
通过将点击事件绑定到父元素上,无论新增或删除了子元素,甚至是动态生成的子元素,都会自动触发相应的事件处理逻辑,大大简化了代码的管理和维护。
⭐说说你常用的ES6新特性
-
块级作用域(Block Scope): 使用
let
和const
关键字声明的变量具有块级作用域,即只在当前代码块中有效。相比于var
声明的变量具有函数作用域而言,这种方式更加灵活和可控。 -
箭头函数(Arrow Function): 箭头函数通过
=>
符号定义,可以简化函数的书写。它具有以下特点:- 自动绑定上下文:箭头函数不会创建自己的
this
,而是继承外层作用域的this
。 - 省略
return
关键字:当函数体只有一条返回语句时,可以省略return
关键字。 - 更简洁的语法:当只有一个参数时,可以省略括号;当函数体为空时,可以使用
() => {}
表示。
- 自动绑定上下文:箭头函数不会创建自己的
-
解构赋值(Destructuring Assignment): 解构赋值允许从数组或对象中提取值并赋给变量。例如,可以通过以下方式提取数组元素:
ini
Codeconst [a, b] = [1, 2];
也可以通过以下方式提取对象属性:
css
Codeconst { name, age } = { name: 'Alice', age: 18 };
- 模板字符串(Template Strings): 模板字符串使用反引号 `` 包裹,并支持在字符串中插入变量或表达式。例如:
ini
Codeconst name = 'Alice';
console.log(`Hello, ${name}!`);
- 默认参数(Default Parameters): 定义函数时可以为参数提供默认值,当调用函数时未传递对应参数时,将使用默认值。例如:
javascript
Codefunction greet(name = 'Alice') {
console.log(`Hello, ${name}!`);
}
greet(); // 输出:Hello, Alice!
- 扩展运算符(Spread Operator): 扩展运算符用于展开可迭代对象(如数组和字符串),将其元素逐个展开,例如:
ini
Codeconst arr = [1, 2, 3];
console.log(...arr); // 输出:1 2 3
- 类(Class): ES6 引入了类语法,可以使用
class
关键字定义一个类。类可以包含构造函数、实例方法、静态方法等。例如:
javascript
Codeclass Person {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(`Hello, my name is ${this.name}!`);
}
static getInfo() {
console.log('This is a person class.');
}
}
const person = new Person('Alice');
person.sayHello(); // 输出:Hello, my name is Alice!
Person.getInfo(); // 输出:This is a person class.
- 简化对象字面量(Object Literal Shorthand): 当定义对象字面量时,如果属性名和变量名相同,可以直接省略写属性值。例如:
ini
Codeconst name = 'Alice';
const age = 18;
const person = { name, age };
console.log(person); // 输出:{ name: 'Alice', age: 18 }
- 模块化(Module): ES6 引入了模块化概念,可以将代码拆分为多个模块,每个模块有自己的作用域,并通过
import
和export
关键字进行模块之间的导入和导出。例如:
javascript
// moduleA.js
export function greet(name) {
console.log(`Hello, ${name}!`);
}
// moduleB.js
import { greet } from './moduleA.js';
greet('Alice'); // 输出:Hello, Alice!
- Promise: Promise 是用于处理异步操作的对象,避免了传统的回调地狱。它表示一个异步操作的最终完成或失败,并可以链式调用
then
、catch
、finally
方法处理结果。例如:
javascript
function fetchData() {
return new Promise((resolve, reject) => {
// 异步操作...
if (/* 异步操作成功 */) {
resolve('Data fetched successfully!');
} else {
reject('Failed to fetch data!');
}
});
}
fetchData()
.then(data => console.log(data)) // 成功时执行
.catch(error => console.error(error)) // 失败时执行
.finally(() => console.log('Done')); // 不论成功与否都执行
- 数组方法(Array Methods): ES6 提供了许多方便的数组方法来处理和遍历数组,如
forEach
、map
、filter
、reduce
等。这些方法提供了更简洁、可读性更高的方式来操作数组。例如:
dart
javascript Codeconst numbers = [1, 2, 3, 4];
numbers.forEach(num => console.log(num)); // 遍历输出每个元素
const doubledNumbers = numbers.map(num => num * 2); // 对每个元素进行操作并返回新数组
const evenNumbers = numbers.filter(num => num % 2 === 0); // 过滤出符合条件的元素
const sum = numbers.reduce((acc, num) => acc + num, 0); // 对数组进行累加
⭐说说WeakMap
感觉这个人讲的比较清楚:WeakMap详解
🌙应用
弱引用:WeakMap的键是弱引用,意味着在没有其他引用指向键时,键会被自动回收,从而释放内存。这对于处理大量临时数据或需要临时缓存的场景非常有用。例如,可以使用WeakMap来存储临时的中间计算结果,当不再需要时自动清除。
隐藏数据:由于WeakMap的键是弱引用的,无法直接遍历获取所有键,因此可以使用WeakMap来隐藏对象的私有数据。通过将对象作为键存储数据,可以确保只有拥有该对象的变量才能访问对应的数据。这样可以有效防止数据被外部修改或访问,提高数据的安全性。
内存管理:WeakMap可以用于处理一些需要手动管理内存的场景。当我们需要在对象之间建立关联关系,并且希望在某个对象被垃圾回收时自动解除关联,就可以使用WeakMap。通过将关联的对象存储为键,可以确保在关联对象被回收时,相关的数据也会自动释放。
需要注意的是,WeakMap的键只能是对象,而不能是原始数据类型(如字符串、数字等)。同时,由于弱引用的特性,WeakMap没有提供像Map那样的迭代方法(如forEach、keys、values等),也没有size属性。所以在使用WeakMap时,需要根据具体场景合理选择,并且注意其特殊的限制和行为。
⭐说一说apply、call、bind
apply
、call
和 bind
都是 JavaScript 中用于改变函数执行上下文(this)的方法,它们的区别如下:
-
apply:
- 语法:
function.apply(thisArg, [argsArray])
- 参数:需要指定函数执行时的上下文对象
thisArg
和可选的参数数组argsArray
。 - 功能:立即调用函数,并改变函数内部的
this
指向thisArg
,同时可以传递一个数组或类数组对象作为参数(通过arguments
对象传递参数也可以)。 - 示例:
fn.apply(obj, [arg1, arg2])
- 语法:
-
call:
- 语法:
function.call(thisArg, arg1, arg2, ...)
- 参数:需要指定函数执行时的上下文对象
thisArg
和可选的多个参数arg1, arg2, ...
。 - 功能:立即调用函数,并改变函数内部的
this
指向thisArg
,同时可以传递多个参数(通过逗号分隔)。 - 示例:
fn.call(obj, arg1, arg2)
- 语法:
-
bind:
- 语法:
function.bind(thisArg, arg1, arg2, ...)
- 参数:需要指定函数执行时的上下文对象
thisArg
和可选的多个参数arg1, arg2, ...
。 - 功能:创建一个新的函数,函数内部的
this
指向thisArg
,并且绑定了指定的参数。不会立即执行函数,而是返回一个新函数,之后可以再次调用。 - 示例:
var boundFn = fn.bind(obj, arg1, arg2)
- 语法:
总结:
apply
和call
可以立即调用函数并改变函数内部的this
指向,区别在于参数传递方式不同,apply
接受参数数组,而call
接受多个参数。bind
不会立即执行函数,而是返回一个新函数,函数内部的this
指向绑定的对象,并且可以预先绑定一些参数。- 这三种方法都可以实现改变函数执行上下文的目的,根据具体需求选择合适的方法。
⭐说一下装箱和拆箱
在 JavaScript 中,基本数据类型(Primitive Types)和引用数据类型(Reference Types)在处理和操作时有所不同。当我们对基本数据类型值进行属性访问或方法调用时,JavaScript 会将基本数据类型自动转换为对应的包装对象,这个过程称为装箱(Boxing)。
下面以数字类型(Number)为例,简要说明基本数据类型装箱的过程:
- 创建包装对象:当我们使用属性或方法来访问数字类型的值时,JavaScript 在后台会创建一个临时的包装对象(Wrapper Object)。对于数字类型,创建的包装对象是 Number 对象。
- 存储值:创建的包装对象将会存储对应的基本数据类型值。例如,对于数字类型的装箱过程,包装对象中会存储相应的数字值。
- 访问属性或方法:一旦装箱完成,我们可以通过包装对象来访问属性和方法。这些属性和方法是与包装对象相关联的。例如,通过
.toFixed()
方法调用来格式化一个数字。 - 自动拆箱:当我们使用包装对象进行操作后,如果需要将结果赋值给一个变量,JavaScript 会自动进行拆箱(Unboxing)操作,将包装对象转换回基本数据类型的值。
装箱的过程是自动发生的,JavaScript 引擎会在需要时自动执行装箱和拆箱操作,使得开发者能够像操作引用类型一样操作基本类型。而这种装箱和拆箱的过程在后台进行,对于开发者来说是透明的。
需要注意的是,由于装箱过程涉及到对象的创建和数据拷贝,相比于直接操作基本类型,使用包装对象会带来额外的开销。因此,在不必要的情况下,最好直接操作基本类型,而不是通过装箱和拆箱操作。
⭐最大安全整数
JavaScript中的最大安全整数是 9007199254740991。它可以使用 Number.MAX_SAFE_INTEGER 常量来表示。这个值是由 JavaScript 中的双精度浮点数表示法决定的,在进行数值运算时不会丢失精度。
超过最大安全整数的数值会导致精度丢失,可能会引发意外的结果。如果需要处理超过最大安全整数范围的大整数,可以使用第三方的大数库或者 BigInt 类型(ES2020 新增)来处理。BigInt 类型可以表示任意精度的整数,但是在进行数值计算时需要特别注意性能和兼容性的问题。
⭐commonjs和es模块有什么区别?
CommonJS(简称CJS)和ES Modules(简称ESM)是两种不同的模块化规范,有以下不同:
-
语法差异:
- CommonJS:使用
require()
导入模块,使用module.exports={xxx}
或exports.xxx=xxx
导出模块。 - ESM:使用
import
导入模块,使用export
导出模块。
- CommonJS:使用
-
对于基本数据类型,ESM导入的是值的引用,CJS导入的是值的拷贝,故ESM中引入的值是常量,不允许直接修改(对象修改或新增属性除外)
- 补充:如果导入的是对象,那么变量中传递来的就是对象的地址值,地址值进行拷贝后通过它访问的还是同一个对象,所以这里对象导入的可以理解为都是引用
- 作为整体导入时(即
import * as mod from xxx
),这时导入的对象第一级是只读的,如果尝试对第一层属性进行修改,就会报错
-
CJS是运行时同步加载(动态),而ESM是编译时异步加载(静态)
-
CJS适用于服务端开发,ESM适用于浏览器端
补充:相同点------CJS和ESM导入时,都会将整个模块的代码执行一遍,然后缓存执行的结果
⭐commonJS中module.exports和exports有什么区别
每个node.js文件在执行时都会隐式的、自动的创造一个module对象,同时,module对象会创建一个名叫exports,初始值为{}的属性:
css
var module = {
exports:{}
}
为了可以更好的导出对应功能模块里的内容,它会又隐式的创建一个变量exports:
java
//隐式的创建了一个变量exports
var exports = module.exports
//此时exports和module.exports 引用的是同一个对象
console.log(exports === modules.exports) //true
所以我们通过exports.xxx = xxx
的方式进行导出,它最终也是被加到了modules.exports对象中
最终导出的是modules.exports对象,这就意味着如果我们将exports的指向改变,那么通过exports.xxx = xxx
添加的导出就不会再添加到modules.exports对象中,xxx也就不会被导出
⭐如何理解JS中的异步
JS是一门单线程语言,因为它运行在浏览器的渲染主线程中(当遇到JS代码时,渲染主线程会将控制权交给js引擎来处理),而渲染主线程只有一个。
而渲染主线程承担着诸多工作,渲染页面,执行js都在其中执行。
如果用同步的方式,极有可能导致主线程产生阻塞,从而导致消息队列中很多任务无法及时执行。
所以浏览器就采取了异步措施,当某些异步任务触发时,如定时器、网络请求、事件监听器等,主线程就会将任务交给其他线程去处理,自身立即结束任务执行,转而执行后续任务,等到异步任务执行完毕,将事先传递的回调函数包装成任务,加入到对应队列(延时队列、微任务队列、交互队列)的末尾排队,等待主线程调度执行。
在这种异步模式下,浏览器就不会发生阻塞,保证了单线程的流畅运行。
⭐说一说事件循环
事件循环又叫消息循环,是浏览器渲染主线程的工作方式。
在Chrome的底层实现中,它开启了一个for的死循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时机将任务放到消息队列中。
以前我们会将消息队列简单分为宏任务队列和微任务队列,现在随着浏览器环境愈发复杂,浏览器采用了更加灵活多变的处理方式。
根据W3C官方解释,每个任务都有不同的类型,同类型的任务须在同一个队列。不同的队列有不同的优先级(例如交互队列优先级大于延时队列),在一次事件循环中,由浏览器自行决定取哪一个队列的任务,但浏览器必须有一个微任务队列,且其优先级是最高的,必须优先调度执行。
⭐JS中的计时器能做到精确计时吗?
不可以
- 计算机中采用晶体振荡器来计时,虽然比较准确,但对比原子钟,精确度还是不足,且会受温度电压等因素影响,在计算机中通常会使用网络时间协议来进行校准。而js计时是调用操作系统中的计时函数,也就携带了些许偏差。
- 按照W3C标准,当浏览器中使用计时器,其嵌套层级超过五层时,之后的计时器就默认会有一个4ms的延迟,这4ms也会引起时间的偏差
- 而最主要的因素还是事件循环中,如果渲染主线程上有任务正在执行,就会推迟计时器的执行
⭐如何判断一个对象是否为空
如果使用常规的Object.getOwnPropertyNames()
来判断,虽然可以将一般的不可枚举类型属性也判断出来,但无法判断继承的属性以及Symbol类型值定义的属性。所以我们需要使用Reflect.ownKeys()
来进行转换,这样通过它返回的数组长度就能精准判断出对象是否为空。
⭐重写一下String.prototype.trim
1、正则
javascript
String.prototype.trim = function() {
return this.replace(/(^\s*)|(\s*$)/g, "")
}
2、双指针法
kotlin
String.prototype.trim = function() {
while(this[0]===' ') {
this = this.subString(1)
}
while(this[this.length-1]===' ') {
this = this.subString(0, this.length-1)
}
}
⭐为什么0.1+0.2!=0.3
- 二进制通过64位 IEEE754存储,但二进制浮点数小数位只能存52位,多出的位数会遵循为1则进位,为0则舍去的规则处理,会导致第一次精度丢失
- 在0.1+0.2时,小数位相加导致小数位又多出一位,又进行1进0舍操作,又造成一次精度丢失
- 综上导致了0.1+0.2!=0.3
⭐什么是内存泄露
定义:内存泄漏是指程序在动态分配内存后,无法释放或回收不再需要的内存空间的现象。当对象或数据被分配内存后,如果程序没有正确释放或回收这些内存空间,就会导致内存泄漏。
内存泄漏的几个常见情况包括:
- 未正确释放事件监听器:如果一个对象被注册为某个事件的监听器,但在不需要它时未手动移除,这将导致该对象无法被垃圾回收。例如,当使用
addEventListener
注册事件监听器时,需要使用removeEventListener
手动移除对应的监听器。 - 闭包中的变量引用:闭包是指函数可以访问并持有其词法环境中的变量。如果在闭包中引用了一些不再使用的变量,这些变量将无法被及时释放。解决方法是确保不再需要的变量解除引用或将其设置为
null
。 - 定时器未清理:使用
setTimeout
或setInterval
创建的定时器,如果不及时清除,会导致函数或对象在定时器仍在运行时不能被垃圾回收。确保在不需要定时器时使用clearTimeout
或clearInterval
清除。 - 循环引用:循环引用是指两个或多个对象相互引用,形成一个闭环,导致垃圾回收器无法判断其中哪个对象可以被释放。这通常发生在对象之间的相互引用,例如父子关系或对象之间的交互。解决方法是在不再需要引用时手动断开循环引用,例如将对象的引用设置为
null
。 - 大量缓存数据:如果程序中大量使用缓存,而没有有效地清理过期或不再使用的缓存数据,这些数据将继续占用内存。需要定期检查和清理不再需要的缓存数据。
⭐说一说闭包
定义:闭包是引用了另一个函数作用域变量的函数,通常是在嵌套函数中实现的。
深层理解:当函数记住并访问了当前所在的词法作用域,并在当前词法作用域之外执行,这时就产生了闭包。
我们常通过闭包来实现:
- 保护变量:通过闭包,可以创建一个私有的作用域,使得外部无法直接访问到其中的变量。这样可以防止变量被意外修改,提高代码的安全性。
- 延长变量生命周期:当一个函数执行完毕后,其内部的变量通常会被销毁。但是如果有闭包存在,那么这些变量将会被保存在闭包中,因此可以在函数执行完毕后继续访问和使用这些变量。
- 实现数据封装:通过闭包,可以模拟实现面向对象编程中的私有属性和方法。将内部的变量和函数封装在闭包中,只暴露需要暴露的接口,隐藏内部实现细节。
- 创建特殊的函数:闭包可以用来创建具有记忆效果的函数,即每次调用都会记住之前的状态和计算结果。这对于某些计算或递归操作非常有用。
- 实现模块化开发:通过使用闭包,可以创建模块化的代码结构。将相关的变量和函数放在闭包中,只暴露对外的接口,避免全局污染和命名冲突。
⭐for循环1000000000000长的数组,如何优化
- 首先可以想到的是:分片+WebWorker多线程
- 分片,每一片分10w条,原理如下
V8引擎会以不同的形式存储数组:
- 快速模式:对应C语言的数组,速度快,紧凑。当索引从0到length-1且无空洞 或预分配数组小于十万,无论有无空洞时采用
- 字典模式:对应C语言的哈希表,速度慢,松散。当预分配数组大于等于十万,且数组有空洞
⭐说说JS的执行上下文
执行上下文是JavaScript代码执行环境的抽象概念,执行上下文可以看作是一个环境,其中包含了当前代码的变量、函数、this 指向等信息。
但要注意执行上下文与作用域的区别:作用域指的是变量、函数和对象在代码中可访问的范围。它定义了变量的可见性和生命周期。作用域规定了在何处以及如何查找变量。
可以说执行上下文是为了实现作用域而存在的一种机制,但它们并不是完全等同的概念。
执行上下文分为三类:
- 全局执行上下文:执行一段JS代码前,会先为它创建全局执行上下文,并将this指向全局对象(在浏览器中是window,node环境中是global)全局执行上下文只会被创建一次,随着程序的开始而创建,随着程序的关闭而销毁。
- 函数执行上下文:执行遇到函数时,就会为这个函数创建一个函数执行上下文,但这里的this和函数执行上下文并不等同,只是上下文中保存有this信息
- Eval 函数执行上下文: 指的是运行在
eval
函数中的代码,很少用而且不建议使用
🌰举个栗子:
csharp
//这里是全局执行上下文
var globalProperty = 0
function fun1() {
//这里是fun1的函数执行上下文
var fun1Property = 1
function fun2() {
//这里是fun2的函数执行上下文
var fun2Property = 2
}
function fun3() {
//这里是fun3的函数执行上下文
var fun3Property = 3
}
}
每次调用函数创建一个新的上下文,会创建一个私有作用域,函数内部声明的任何变量都不能在当前函数作用域外部直接访问。
例如上例中:
- fun1中可以访问全局执行上下文中的globalProperty,而不能访问内部fun2 fun3中的变量
- 同理,fun2 fun3可以访问外部的fun1Property以及最外层的globalProperty
执行上下文中存了些什么?
在执行上下文被创建时:
- 绑定this,普通声明的函数在执行时才能确定绑定的this值,而es6中箭头函数是词法绑定,被创建时就绑定了所在上下文的this
- LexicalEnvironment(词法环境) 组件创建
- VariableEnvironment(变量环境) 组件创建
ini
//伪代码流程
ExecutionContext = {
ThisBinding = <this value>, // 确定this
LexicalEnvironment = { ... }, // 词法环境
VariableEnvironment = { ... }, // 变量环境
}
词法环境:
词法环境是一种持有标识符---变量的映射的结构(简单理解为就是保存了我们定义的变量和函数)
词法环境的组成:
- 环境记录器:是存储变量和函数声明的实际位置。
- 外部环境的引用:意味着它可以访问其父级词法环境。
词法环境有两种类型:
- 全局环境:没有外部环境引用的词法环境(环境记录器指向空),它里面保存了JS内置对象、用户定义的全局变量/函数,this绑定为全局对象。
- 函数环境:用户在函数内部定义的变量存储在环境记录器中,外部环境引用的是外部执行上下文。
根据词法环境的类型不同,环境记录器也分为两类:
-
全局环境中------对象环境记录器:用来定义出现在全局上下文中的变量和函数的关系,当一个变量或函数在对象环境中被定义或声明时,对象环境记录器会使用一个对象来管理这些变量和函数的绑定关系,通过关联的对象,对象环境记录器实现了对变量和函数的访问和操作 🌰举个栗子:
javascriptvar obj = { x: 10, y: function() { console.log('Hello'); } }; console.log(obj.x); // 输出:10 obj.y(); // 输出:Hello
在这个例子中,
obj
对象包含了x
属性和y
方法。通过对象环境记录器,我们可以通过obj.x
访问对象的属性值,并通过obj.y()
调用对象的方法。 -
函数环境中------声明式环境记录器:存储变量、函数和参数。
词法环境与对象环境记录器在内部机制上有所不同。其中最明显的区别是词法环境使用一个特殊的数据结构------词法环境记录器(Lexical Environment Record),而对象环境记录器则使用一个普通的 JavaScript 对象
dart
//词法环境伪代码
GlobalExectionContext = { // 全局执行上下文
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录器:存储变量和函数声明的实际位置
Type: "Object",
// 在这里绑定标识符
}
outer: <null> // 对外部环境的引用:可以访问其父级词法环境
}
}
FunctionExectionContext = { // 函数执行上下文
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
}
outer: <Global or outer function environment reference>
}
}
变量环境:
变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性
在 ES6 中,词法环境和变量环境的区别在于前者用于存储函数声明和变量( let
和 const
)绑定,而后者仅用于存储变量( var
)绑定
例如:
ini
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
👇执行上下文
yaml
GlobalExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: { // 词法环境
EnvironmentRecord: {
Type: "Object",
// 在这里绑定标识符
a: < uninitialized >, // let、const声明的变量
b: < uninitialized >, // let、const声明的变量
multiply: < func > // 函数声明
}
outer: <null>
},
VariableEnvironment: { // 变量环境
EnvironmentRecord: {
Type: "Object",
// 在这里绑定标识符
c: undefined, // var声明的变量
}
outer: <null>
}
}
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: { // 词法环境
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
Arguments: {0: 20, 1: 30, length: 2}, // arguments对象
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: { // 变量环境
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
g: undefined // var声明的变量
},
outer: <GlobalLexicalEnvironment>
}
}
在其中,let和const定义的关联值设为了uninitialized,说明为初始化,而var则被初始化为了undefined,这就是let/const和var的本质区别
综上,JS程序执行的全过程大致可以描述为:
程序启动,全局执行上下文被创建,压入调用栈
-
创建全局上下文的 词法环境
- 创建 对象环境记录器 ,它用来定义出现在 全局上下文 中的变量和函数的关系(负责处理
let
和const
定义的变量) - 创建 外部环境引用 ,值为
null
- 创建 对象环境记录器 ,它用来定义出现在 全局上下文 中的变量和函数的关系(负责处理
-
创建全局上下文的 变量环境
- 创建 对象环境记录器 ,它持有 变量声明语句 在执行上下文中创建的绑定关系(负责处理
var
定义的变量,初始值为undefined
造成声明提升) - 创建 外部环境引用 ,值为
null
- 创建 对象环境记录器 ,它持有 变量声明语句 在执行上下文中创建的绑定关系(负责处理
-
确定
this
值为全局对象(以浏览器为例,就是window
)
函数被调用,函数执行上下文被创建,压入调用栈
-
创建函数上下文的 词法环境
- 创建 声明式环境记录器 ,存储变量、函数和参数,它包含了一个传递给函数的
arguments
对象(此对象存储索引和参数的映射)和传递给函数的参数的 length 。(负责处理let
和const
定义的变量) - 创建 外部环境引用,值为全局对象,或者为父级词法环境(作用域)
- 创建 声明式环境记录器 ,存储变量、函数和参数,它包含了一个传递给函数的
-
创建函数上下文的 变量环境
- 创建 声明式环境记录器 ,存储变量、函数和参数,它包含了一个传递给函数的
arguments
对象(此对象存储索引和参数的映射)和传递给函数的参数的 length 。(负责处理var
定义的变量,初始值为undefined
造成声明提升) - 创建 外部环境引用,值为全局对象,或者为父级词法环境(作用域)
- 创建 声明式环境记录器 ,存储变量、函数和参数,它包含了一个传递给函数的
-
确定
this
值
进入函数执行上下文的执行阶段:
- 在上下文中运行/解释函数代码,并在代码逐行执行时分配变量值。
参考文章:
彻底搞懂作用域、执行上下文、词法环境 - 掘金 (juejin.cn)
面试官:JavaScript中执行上下文和执行栈是什么? | web前端面试 - 面试官系列 (vue3js.cn)
shell
### ⭐说说JS的执行栈
JavaScript 中的执行栈(Execution Stack),也称为调用栈(Call Stack),是一种用来管理函数执行上下文的数据结构。它的主要作用是跟踪函数的执行顺序和管理函数执行期间的内存分配。
当 JavaScript 程序执行时,会创建一个全局执行上下文并将其推入执行栈的底部。每当遇到一个函数调用时,就会创建该函数的执行上下文,并将其推入执行栈的顶部。执行栈采用先进后出的顺序工作,因此当前执行的函数总是位于栈的顶部。
执行栈的主要作用包括:
- 跟踪函数的执行顺序:通过执行栈,JavaScript 引擎可以追踪函数的调用关系,确保函数按照正确的顺序执行。当一个函数被调用时,它会被推入执行栈的顶部,当函数执行完毕时,它会被弹出执行栈。
- 管理内存分配:每个函数在执行期间需要一些内存空间来存储变量、参数和临时的计算结果。执行栈负责为每个函数分配和释放内存空间,确保不同函数之间的数据不被混淆或冲突。
- 处理函数的返回值:当一个函数完成执行并从执行栈中弹出时,它可以将一个值作为返回值传递给调用它的上一级函数。执行栈负责接收、传递和处理这些返回值,以便程序可以继续执行。
⭐如何撤销axios请求
执行axios.CancelToken.source()方法获取source对象,这个对象上就有一个cancel方法用于取消请求,原理是利用XHR对象中的abort方法来已经发出的请求进行取消,调用XHR.abort()之后,重置xhr.status状态值为0(响应成功状态值为4),避免走成功响应逻辑
javascript
const axios = require('axios');
// 创建一个 CancelToken.source 实例
const source = axios.CancelToken.source();
// 发起请求
axios.get('https://api.example.com/data', {
cancelToken: source.token
})
.then(response => {
// 请求成功处理逻辑
console.log(response.data);
})
.catch(error => {
// 捕获取消请求的错误
if (axios.isCancel(error)) {
console.log('请求被取消:', error.message);
} else {
console.log('请求发生错误:', error.message);
}
});
// 取消请求
source.cancel('请求被用户取消');
⭐XHR使用流程
-
创建 XMLHttpRequest 对象 :使用
new XMLHttpRequest()
创建一个新的 XMLHttpRequest 对象。iniconst xhr = new XMLHttpRequest();
-
设置请求配置:通过调用 XHR 对象的方法和属性来配置请求。常见的配置包括请求的类型、URL、是否异步等。
kotlinxhr.open('GET', 'https://api.example.com/data', true); xhr.setRequestHeader('Content-Type', 'application/json');
-
设置回调函数 :使用
onreadystatechange
事件处理程序或其他事件处理程序来定义请求状态的变化时应执行的操作。通常,你需要在 readyState 变为 4(请求已完成)且 status 是 200(OK)的情况下执行相应的操作。inixhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { // 请求成功,执行相应操作 console.log(xhr.responseText); } };
-
发送请求 :使用
send()
方法发送请求。对于 GET 请求,可以将参数作为 URL 查询字符串的一部分;对于 POST 请求,可以将参数作为 send() 方法的参数。scssxhr.send();//send中可以传入json作为请求体
-
处理响应 :在回调函数中,根据需要处理服务器响应。可以使用
responseText
、responseXML
或response
属性来获取服务器响应的内容。arduinoconsole.log(xhr.responseText);
完整的使用示例:
ini
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText);
}
};
xhr.send();
⭐XHR的状态
- 0 - 未初始化(UNSENT) : 初始状态,XMLHttpRequest对象已创建但尚未调用open()方法。
- 1 - 打开(OPENED) : open()方法已被调用。在该状态下,可以通过调用setRequestHeader()方法设置请求头信息。
- 2 - 发送(HEADERS_RECEIVED) : 已调用send()方法并且已经接收到响应头信息。此时可以通过getResponseHeader()方法获取响应头信息。
- 3 - 接收(LOADING) : 响应体正在被接收。在此阶段可以通过responseText属性获取部分响应数据。
- 4 - 完成(DONE) : HTTP请求已完成,且响应数据已完全接收。此时可以使用responseText和responseXML属性获取完整的响应数据。
⭐为什么使用XHR时选择监听onreadystatechange而不是load回调?
- 更全面的状态监控:
readystatechange
事件提供了对请求状态的更详细监控。通过检查readyState
属性的值,可以在请求的不同阶段执行不同的操作。例如,在状态为2时可以获取到响应头信息,而在状态为4时可以获取到完整的响应数据。这使得开发人员能够更精确地控制和处理请求过程中的各个阶段。 - 处理非成功状态码: 通过
readystatechange
事件,可以检查status
属性来获取请求的状态码。除了成功状态码(如200),还可以获取到其他状态码(如404、500等)。这使得开发人员能够针对不同的状态码执行相应的错误处理逻辑,以便更好地处理请求失败的情况。
而使用 load
回调函数可以更方便地处理请求成功的情况,一旦请求成功,回调函数就会被触发。这种方式适合于那些只关注请求成功并需要处理成功响应的场景。
因此,选择使用 readystatechange
事件还是 load
回调函数主要取决于具体的需求。如果需要更细粒度地监控请求的不同状态,或者需要处理不同的状态码,那么使用 readystatechange
事件是更合适的。如果只需要处理请求成功的情况,可以使用 load
回调函数来简化代码逻辑。
⭐为什么typeof null是对象
typeof null
的结果是 "object"
是因为在 JavaScript 的早期实现中,使用一个 32 位的二进制表示变量的类型信息。其中,000 开头的二进制表示对象,而 null 的二进制表示全是 0,也就是空指针。由于 null 在 JavaScript 中被视为一个空对象指针,因此在 typeof
操作符的处理中,将其错误地判断为对象。
这个错误最初出现在 JavaScript 语言的设计中,并且由于历史原因,为了保持向后兼容性,这个错误一直保留至今。虽然 typeof null 返回 "object" 在语义上并不准确,但它已经成为 JavaScript 中的一个特例。
需要注意的是,除了 null 以外,其他类型的值在 typeof
的返回结果中通常是符合预期的。例如,typeof
对于数值(number)、字符串(string)、布尔值(boolean)、函数(function)和未定义(undefined)等类型的值都能正确返回对应的类型。
当我们需要检查一个值是否为 null 时,应该使用严格相等运算符 ===
进行判断,而不是通过 typeof
操作符。
⭐相比于es5构造函数,ES6类的优点
- 更加简洁易读:ES6 中的类语法更加简洁易读,减少了无意义的代码量和原型链操作。
- 更加符合面向对象编程的语法:ES6 类的语法更加符合传统面向对象语言的语法,包括类的继承、封装和多态等特性。
- 支持基于 extends 关键字的继承:ES6 类支持基于 extends 关键字的继承,可以更方便地实现类之间的继承关系。
- 支持构造函数和静态方法:ES6 类中可以使用 constructor 方法定义构造函数,在类的外部使用 static 关键字定义静态方法,使用更加方便。
- 作用域限制更明确:在 ES6 中,类声明不会将类名绑定到当前作用域中,使得代码的作用域限制更加明确。
⭐说一说JS的模块化
在ES5时,ES模块化规范还未出现,需要我们自己通过利用JS的作用域来实现模块化,大致有这几种形式:
- try...catch的catch自带作用域
- 简单的模块化可以使用IIFE,在立即执行函数中会生成一个函数作用域,外界就无法进行访问,可以做到对要隔离的代码进行分隔
- 如果是需要封装功能模块就可以考虑闭包,通过定义一个函数,将他的函数作用域作为模块,将该函数中的子函数作为要向外暴露的API进行返回,这样外界就能并且只能通过这个API来访问处理模块中的数据
到了ES6就出现了ES模块化规范,可以通过一个文件一个模块的形式进行导入和导出,这样做的好处是这样的模式是一个能被稳定识别的模式,而函数模块则是编译器无法识别的模式,ES6模块API更加稳定,且在编译期就会检查对导入模块API成员的引入是否存在,如果引用不存在,就可以在编译运行时抛出"早期"错误,而不是如函数模块化API一样只有在运行时才会被考虑进来。
⭐如何解决for(var i = 0;i < 5;i++){setTimeout(()=>{console.log(i)})}的问题
因为var声明的i是挂载外层的,而不是在for的块里面,就导致了最后输出i时访问的是累加后最终的i,所以解决思路是创建块级作用域:
-
使用let来定义i
-
使用函数来包裹
cssfor(var i = 0;i < 5;i++){ setTimeout(function(i){ console.log(i) },0,i) //第三个参数i为向function中传的参 }
扩展:try...catch和with也会生成块级作用域,在一些es6转es5的实现中,就是用try..catch来进行块级作用域的转换
⭐反转数组
- reverse原地反转 / from + reverse 浅拷贝后反转
- for反向遍历+push
- reduce遍历反向加
- 双指针左右两两交换
⭐判断对象为空
- for in 遍历------拿不到Symbol键
- Reflect.ownkeys().length------准确判断
⭐如何消除异步的传染性
异步的传染性理解起来就是,多个嵌套函数中,内层函数若有异步操作,则外层的所有函数都将变为异步函数
- 封装一个包装函数,接收一个参数,参数为我们要消除传染性的一系列函数的入口
- 在包装函数内设置缓存,缓存中放置请求结果,一开始缓存为null(如果是多个数据,可以以一个缓存数组的方式定义),并设立状态表示缓存状态是否完成
- 包装函数中重写发送请求原生的逻辑,例如改写window上的fetch,判断是否存在对应缓存,若存在直接返回
- 如果执行时不存在缓存,就通过原生fetch发送请求,拿到对应的promise,为其定义完成回调,在回调中进行缓存的设置和缓存状态的变更,然后将这个promise作为错误抛出
- 以上都定义完毕后,就进行入口函数的调用,注意调用使用try...catch块进行错误捕获,这样就可以捕获到在fetch发送请求后抛出的异常
- 在catch块中拿到promise对象,为其注册回调为入口函数,这样当promise完成后,就可以再次调用入口函数,重新走一遍流程,而这次,我们是有备(缓存)而来的
react的suspense异步组件也是这个解决思路👆,而Vue3不同,在 Vue3 中,并不是通过抛出 Promise 错误来实现的,而是通过捕获 Promise 状态变化来确定何时展示实际内容。这两个框架的实现方式是不同的
bash
## 🌙Vue
⭐Vue2响应式的实现
我的博客里有讲:小何的世界 (theluckyone.top)
⭐Vue2与Vue3的区别
🌙生命周期
Vue3中取消了beforeCreated和created,添加了setup函数,其负责设置组件的初始状态、响应式数据和执行其他逻辑。它在组件实例被创建之前被调用,并且接收两个参数:props
和 context
:
props
参数表示组件接收到的属性(props
),并且是只读的。context
参数包含了一些常用的属性和方法,如attrs
、slots
、emit
等。
由于setup就是初始化时执行的函数,所以可以替代beforeCreated和created的效用
常用生命周期对比:
vue2 | vue3 |
---|---|
beforeCreate | |
created | |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeDestroy | onBeforeUnmount |
destroyed | onUnmounted |
并且再Vue3中提供了onServerPrefetch钩子,若在其中返回了一个Promise,服务端就可以等待Promise完成后再对该组件进行渲染
🌙组合式API
Vue2 是选项API(Options API),一个逻辑会散乱在文件不同位置(data、props、computed、watch、生命周期钩子等),导致代码的可读性变差。当需要修改某个逻辑时,需要上下来回跳转文件位置。
Vue3 组合式API(Composition API)则很好地解决了这个问题,可将同一逻辑的内容写到一起,增强了代码的可读性、内聚性,其还提供了较为完美的逻辑复用性方案。
🌙异步组件加载
在 Vue 3 中,异步组件加载可以使用 Suspense
组件和 async setup
功能来实现。Suspense
组件可以用于处理异步组件的加载和显示占位符内容,而 async setup
可以在异步组件加载完成后执行相关逻辑。
下面是使用 Suspense
和 async setup
的示例:
首先,在父组件中使用 Suspense
包裹需要异步加载的子组件,并设置一个占位符(fallback)内容:
xml
<template>
<Suspense>
<template #default>
<!-- 异步加载的子组件 -->
<AsyncComponent />
</template>
<template #fallback>
<!-- 加载时的占位符内容 -->
<div>Loading...</div>
</template>
</Suspense>
</template>
然后,在异步加载的子组件中使用 async setup
来定义组件的逻辑:
javascript
export default {
async setup() {
// 异步加载资源或执行其他异步操作
await someAsyncOperation();
// 返回组件的响应式数据和方法
return {
data: reactive({}),
method() {
// ...
}
};
}
};
</script>
在上述示例中,当父组件渲染时,Suspense
组件会检测到子组件的异步加载,并显示占位符内容("Loading...")。一旦异步加载完成,Suspense
组件会自动切换到子组件的内容,并显示子组件的内容。
通过使用 Suspense
和 async setup
,你可以更方便地处理异步组件加载过程中的状态,并在加载完成后执行相关逻辑。
🌙Teleport
传统的 Vue 组件会被渲染到其父组件的 DOM 结构内,但是有时我们希望将组件渲染到 DOM 结构的不同位置,而不需要改变其父组件的布局。这就是 Teleport
的作用所在。
下面是一个使用 Teleport
的示例:
xml
<template>
<div>
<!-- 在页面的 body 元素下渲染 Modal 组件 -->
<Teleport to="body">
<Modal />
</Teleport>
<!-- 其他组件的内容 -->
<div>
...
</div>
</div>
</template>
在上述示例中,Teleport
组件将 Modal
组件渲染到了页面的 body
元素下。无论在组件内部的哪个位置使用 Teleport
,最终 Modal
组件都会被渲染到 body
元素下,而不是组件的父容器内部。
通过使用 Teleport
,你可以将组件渲染到任何合法的 DOM 元素位置,而不局限于其父组件的 DOM 结构。这对于实现全局的弹窗、模态框或通知等功能非常有用。
需要注意的是,Teleport
需要配合 to
属性来指定组件渲染的目标位置,该位置可以是一个有效的 CSS 选择器,或者是特殊的预定义值,如 "body"
、"#app"
等。
另外,需要确保目标位置在组件被挂载的时候是存在的,否则 Teleport
会抛出警告并在渲染期间失效。
总结来说,Teleport
组件使你能够非常灵活地将组件渲染到任意位置,而不受其父组件的限制。这为处理全局弹窗、模态框等提供了方便的解决方案。
🌙响应式原理
vue2的响应式原理在上面提过了
Vue 3 的底层响应式实现原理主要通过 Proxy 对象和依赖追踪来实现。
- Proxy 对象:Vue 3 使用了 ES6 中的 Proxy 对象来实现响应式。Proxy 对象可以拦截 JavaScript 对象上的各种操作,比如读取属性、设置属性、删除属性等。通过使用 Proxy 对象,Vue 3 可以在属性发生变化时捕获到对应的操作,并触发更新。
- 依赖追踪:Vue 3 使用了依赖追踪的机制来追踪属性之间的依赖关系。每个响应式对象都有一个关联的依赖追踪器,当访问响应式对象的某个属性时,会将当前的依赖关系添加到依赖追踪器中。这样,在属性发生变化时,可以找到所有依赖于该属性的地方,并触发相应的更新。
🌙新增patchFlag字段
patchFlag
字段是一个位掩码,通过对不同位进行组合来表示不同的变化类型。它可以帮助 Vue 3 在进行 diff 运算时更高效地定位和处理需要更新的部分,避免不必要的比较和操作,从而提升性能。
具体作用如下:
- 快速判断节点类型变化:
patchFlag
可以告诉 Vue 3 虚拟 DOM 的变化类型,包括节点的类型、文本内容、元素的动态属性等。通过检查patchFlag
的不同位,可以快速判断节点类型是否发生了变化,避免进行不必要的深度比较。 - 提高 diff 粒度:通过细分不同的
patchFlag
值,可以将节点的比较和更新粒度调整得更加准确。例如,如果只有属性发生了变化,可以仅针对属性进行比较,而无需再对子节点进行深度比较。 - 优化响应式更新:通过
patchFlag
标记节点的变化,Vue 3 可以针对性地处理不同类型的更新。例如,当一个组件的props
发生变化时,只需针对props
进行更新,而无需重新渲染整个组件。
类型列举:
ini
// patchFlags 字段类型列举
export const enum PatchFlags {
TEXT = 1, // 动态文本内容
CLASS = 1 << 1, // 动态类名
STYLE = 1 << 2, // 动态样式
PROPS = 1 << 3, // 动态属性,不包含类名和样式
FULL_PROPS = 1 << 4, // 具有动态 key 属性,当 key 改变,需要进行完整的 diff 比较
HYDRATE_EVENTS = 1 << 5, // 带有监听事件的节点
STABLE_FRAGMENT = 1 << 6, // 不会改变子节点顺序的 fragment
KEYED_FRAGMENT = 1 << 7, // 带有 key 属性的 fragment 或部分子节点
UNKEYED_FRAGMENT = 1 << 8, // 子节点没有 key 的fragment
NEED_PATCH = 1 << 9, // 只会进行非 props 的比较
DYNAMIC_SLOTS = 1 << 10, // 动态的插槽
HOISTED = -1, // 静态节点,diff阶段忽略其子节点
BAIL = -2 // 代表 diff 应该结束
}
shell
#### 🌙事件缓存
Vue3 的cacheHandler
可在第一次渲染后缓存我们的事件。相比于 Vue2 无需每次渲染都传递一个新函数。
例如我们下面加一个 click 事件:
xml
<div id="app">
<h1>vue3事件缓存讲解</h1>
<p>今天天气真不错</p>
<div>{{name}}</div>
<span onCLick=() => {}><span>
</div>
渲染函数如下所示。
javascript
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from vue
const _withScopeId = n => (_pushScopeId(scope-id),n=n(),_popScopeId(),n)
const _hoisted_1 = { id: app }
const _hoisted_2 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(h1, null, vue3事件缓存讲解, -1 /* HOISTED */))
const _hoisted_3 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(p, null, 今天天气真不错, -1 /* HOISTED */))
const _hoisted_4 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(span, { onCLick: () => {} }, [
/*#__PURE__*/_createElementVNode(span)
], -1 /* HOISTED */))
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(div, _hoisted_1, [
_hoisted_2,
_hoisted_3,
_createElementVNode(div, null, _toDisplayString(_ctx.name), 1 /* TEXT */),
_hoisted_4
]))
}
观察以上渲染函数,你会发现 click 事件节点为静态节点(HOISTED 为 -1),即不需要每次重新渲染。
🌙diff算法优化
patchFlag 帮助 diff 时区分静态节点,以及不同类型的动态节点。一定程度地减少节点本身及其属性的比对。
🌙打包优化
Tree-shaking:模块打包 webpack、rollup 等中的概念。移除 JavaScript 上下文中未引用的代码。主要依赖于 import 和 export 语句,用来检测代码模块是否被导出、导入,且被 JavaScript 文件使用。
以 nextTick 为例子,在 Vue2 中,全局API暴露在Vue实例上,即使未使用,也无法通过 tree-shaking 进行消除。
javascript
import Vue from 'vue';
Vue.nextTick(() => {
// 一些和DOM有关的东西
});
Vue3 中针对全局和内部的API进行了重构,并考虑到 tree-shaking 的支持。因此,全局API现在只能作为ES模块构建的命名导出进行访问。
javascript
import { nextTick } from 'vue'; // 显式导入
nextTick(() => {
// 一些和DOM有关的东西
});
通过这一更改,只要模块绑定器支持 tree-shaking,则Vue应用程序中未使用的 api 将从最终的捆绑包中消除,获得最佳文件大小。
受此更改影响的全局API如下所示。
- Vue.nextTick
- Vue.observable (用 Vue.reactive 替换)
- Vue.version
- Vue.compile (仅全构建)
- Vue.set (仅兼容构建)
- Vue.delete (仅兼容构建)
内部API也有诸如 transition、v-model 等标签或者指令被命名导出。只有在程序真正使用才会被捆绑打包。Vue3 将所有运行功能打包也只有约22.5kb,比 Vue2 轻量很多。
🌙支持多个根节点
⭐Vue.js是什么?请简要介绍一下Vue.js的特点
Vue.js是一款基于MVVM架构设计的渐进式JavaScript框架。它通过数据驱动视图的方式实现响应式,在开发过程中能够自动更新页面上的变化,无需手动操作DOM。Vue.js采用组件化开发模式,将界面拆分为多个独立的可复用组件,大大提高了代码复用性和可维护性。此外,Vue.js还支持单页面应用开发,通过内置的路由系统实现页面间的切换和无刷新加载,提供了良好的用户体验。为了克服首屏加载慢和SEO不友好等问题,可以使用Nuxt.js来进行服务器端渲染。在Vue 3中,推出了组合式API、diff优化、事件缓存、打包优化等新特性,进一步简化了开发流程,提升了性能和开发效率。
⭐Vue2的组件模板中为什么只允许有一个根元素
vue2模板最后会被转换为VDOM,而VDOM是一个树形结构,所以需要唯一的根元素
在vue3中支持了多个根组件的写法,原理是在编译阶段新增了判断,如果当前组件不只一个根元素,就添加一个 fragment
组件把这个多根组件的结构给包起来,相当于这个组件还是只有一个根节点
⭐解释一下Vue实例的生命周期钩子函数及其作用。
Vue.js的生命周期钩子函数用于在不同阶段执行特定的任务。在Vue 2中,常用的生命周期钩子有:beforeCreate(实例刚创建,但尚未初始化),created(实例已创建完成,可以访问data和methods等属性),beforeMount(在DOM挂载之前调用),mounted(在DOM挂载完成后调用),beforeUpdate(数据更新之前调用),updated(数据更新完成后调用),beforeDestroy(实例销毁之前调用),destroyed(实例已销毁)。在Vue 3中,部分生命周期钩子函数的名称发生了变化,并删除了beforeDestroy和destroyed钩子,新增了onBeforeUnmount和onUnmounted钩子。
⭐Vue组件之间的通信方式有哪些?请分别介绍它们的应用场景。
父子组件:
- props传值,适用于父组件传递数据给子组件
- 自定义事件$emit,适用于子组件传递数据给父组件
- 子组件定义ref,父组件获取到子组件实例取到数据,适用于子组件传递数据给父组件
兄弟组件:
-
通过共同父组件或共同根组件进行通信,适用于兄弟组件 兄弟组件:
csharpthis.$parent.on('add',this.add)
另一个兄弟组件:
kotlinthis.$parent.emit('add')
-
EventBus总线,适用于兄弟组件传值
祖先与后代:
-
<math xmlns="http://www.w3.org/1998/Math/MathML"> a t t r s 与 attrs 与 </math>attrs与listeners,适用于隔代组件通信
$attrs中存有所有父组件传给子组件但子组件未通过props传递的值
$listeners中存有所有父组件中的(没有使用.native修饰的)v-on事件监听器
父组件传给子组件,子组件通过
v-bind='$attrs'
和v-on='$listeners'
将两者传给孙子组件,孙子组件就可以获取对应数据和事件监听器 -
provide 与 inject,适用于祖先组件为后代组件传值
javascript//祖先节点中,也可以写成对象形式 provide(){ return { foo:'foo' } }
arduinoinject:['foo'] // 获取到祖先组件传递过来的值
复杂情况:
通过vuex实现全局共享
⭐什么是Vue的计算属性(Computed)?与方法(Methods)有何区别?在什么情况下应该使用计算属性?
计算属性是Vue.js提供的一种便捷的属性计算方式。它会根据依赖的响应式数据自动进行计算,并缓存计算结果,在依赖没有发生变化时返回缓存的结果,从而避免不必要的重复计算。这对于那些依赖多个响应式数据进行复杂逻辑计算的情况下非常有用。而在方法中进行同样的计算,每次需要手动调用方法,而且每次调用都会重新执行计算逻辑,没有缓存机制,会导致不必要的性能损耗。因此,在需要频繁使用、且依赖数据具有变动性的情况下,通常应该使用计算属性。
⭐Vue中的路由是如何实现的?请简要介绍Vue的路由功能以及常用的路由配置选项。
-
Hash 模式:
- 使用 URL 中的 hash(#)来模拟路由,并通过
window.location.hash
来监听变化。 - 例如,一个使用 Hash 模式的路由路径可能是
http://example.com/#/home
。 - Hash 模式下,每次路由发生变化时,URL 中的 hash 值会更新,但不会向服务器发送请求。
- 在 Vue.js 中,可以通过创建
VueRouter
实例时设置mode: 'hash'
来启用 Hash 模式。
- 使用 URL 中的 hash(#)来模拟路由,并通过
-
History 模式:
- 使用 HTML5 提供的
history.pushState
和history.replaceState
方法来模拟路由变化。 - 例如,一个使用 History 模式的路由路径可以是
http://example.com/home
。 - History 模式下,路由变化时,实际上是修改了浏览器历史记录中的当前页面路径,不会带有特殊字符(如 #)。
- 注意,在使用 History 模式时,需要服务器配置支持,以便在直接访问路由路径时返回正确的页面。
- 在 Vue.js 中,可以通过创建
VueRouter
实例时设置mode: 'history'
来启用 History 模式。
- 使用 HTML5 提供的
对于选择使用哪种模式,可以考虑以下因素:
- Hash 模式在老版本的浏览器中兼容性更好。
- History 模式会带来更加友好的 URL 形式,但需要服务器配置支持。
- 如果不关心 URL 形式,或者在开发纯前端应用时,可以使用默认的 Hash 模式。
另外,可以通过base
选项指定基本路径,linkActiveClass
选项设置当前激活链接的class,scrollBehavior
选项用于记录滚动位置。使用routes
配置路由路径和对应的组件,可以使用对象数组形式,包括path
和component
等参数。此外,也可以使用函数方式来引入组件,以实现懒加载的效果。
⭐父子组件生命周期的先后关系
生命周期(父子组件) 父组件beforeCreate --> 父组件created --> 父组件beforeMount --> 子组件beforeCreate --> 子组件created --> 子组件beforeMount --> 子组件 mounted --> 父组件mounted -->父组件beforeUpdate -->子组件beforeDestroy--> 子组件destroyed --> 父组件updated
加载渲染过程 父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
挂载阶段 父created->子created->子mounted->父mounted
父组件更新阶段 父beforeUpdate->父updated
子组件更新阶段 父beforeUpdate->子beforeUpdate->子updated->父updated
销毁阶段 父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
⭐computed与watch
-
关联:
computed
:computed
是 Vue.js 提供的一个属性计算方式。通过定义computed
属性,可以根据依赖的响应式数据自动计算衍生值,并且只有在相关数据发生变化时才重新计算。computed
属性是基于响应式依赖进行缓存的,只有依赖的数据发生改变时才会重新计算。watch
:watch
是 Vue.js 提供的一个观察属性变化的功能。通过定义watch
属性,可以监听指定的数据变化并执行相应的回调函数。watch
是基于观察的方式,当被监听的数据发生变化时,回调函数将被立即执行。
-
适用情况:
computed
:适用于需要根据已有响应式数据计算衍生值的场景,例如对数据进行格式化、过滤、排序等操作。由于computed
属性是基于依赖的缓存机制,可以有效地减少不必要的计算,并提高性能。watch
:适用于需要在数据变化时执行异步或开销较大的操作,或者需要监听非响应式数据的变化。watch
允许你执行任意逻辑,包括调用接口、发起网络请求等操作。
-
优劣势:
-
computed的优势:
- 缓存机制:
computed
属性会基于依赖进行缓存,只有当依赖发生变化时才会重新计算,避免了不必要的重复计算。 - 自动更新:对于依赖的响应式数据的变化,
computed
属性会自动更新。 - 声明式:通过声明
computed
属性,可以更直观地描述数据之间的关系。
- 缓存机制:
-
watch的优势:
- 异步和副作用:
watch
属性可以处理异步或有副作用的操作,例如调用接口、发起网络请求等。 - 监听非响应式数据:
watch
可以监听非响应式数据的变化。 - 灵活性:
watch
允许执行任意逻辑,更加灵活。
- 异步和副作用:
-
需要根据具体情况选择使用 computed
还是 watch
。通常情况下,如果需要根据已有的响应式数据计算衍生值,使用 computed
更加合适;如果需要监听数据变化并执行异步或开销较大的操作,使用 watch
更加适合。
⭐组件中的data为什么是函数
在 Vue.js 组件中,data
通常被定义为一个函数,而不是一个直接的对象。这是因为组件在 Vue 中是可复用的,当多个实例共享同一个组件时,每个实例都需要拥有自己独立的数据。如果在组件中直接使用对象形式的 data
,那么所有实例将共享同一个数据对象,从而导致数据混乱和不可预料的行为。
通过将 data
定义为函数,每个组件实例都会调用该函数来返回一个独立的数据对象。这样每个实例就可以拥有自己的数据副本,彼此之间互不干扰。
示例:
javascript
Vue.component('my-component', {
data: function() {
return {
message: 'Hello, World!'
};
}
});
当组件被创建的时候,data
函数会被执行,返回一个新的数据对象给该组件的实例。这样,在每次创建该组件实例时,都会得到一个全新的数据对象,确保数据的独立性和隔离性。
值得注意的是,如果使用 ES6 的箭头函数来定义 data
,会导致箭头函数内部的 this
不再指向组件实例。因此,为了确保在 data
函数内部能够访问到正确的组件实例,应该使用普通函数来定义 data
。
总结 : 将组件的 data
定义为函数是为了确保每个组件实例都拥有独立的数据对象,避免数据混乱和共享引发的问题。每次创建组件实例时,都会调用该函数返回一个全新的数据对象,确保数据隔离和独立性。
⭐为什么v-for和v-if不建议用在一起
1.当 v-for 和 v-if 处于同一个节点时,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。如果要遍历的数组很大,而真正要展示的数据很少时,这将造成很大的性能浪费(Vue2.x) 2.这种场景建议使用 computed,先对数据进行过滤
注意:3.x 版本中 v-if
总是优先于 v-for
生效。由于语法上存在歧义,建议避免在同一元素上同时使用两者。比起在模板层面管理相关逻辑,更好的办法是通过创建计算属性筛选出列表,并以此创建可见元素。
⭐React/Vue 项目中 key 的作用
在 React 和 Vue 项目中,key
属性用于标识列表中每个元素的唯一性。它在 Virtual DOM 的更新过程中扮演了重要的角色,具有以下作用:
- 识别组件:
key
用于帮助 React 或 Vue 更准确地识别列表项或动态生成的组件,并确定何时重新渲染或更新特定的组件。通过设置唯一的key
值,React 或 Vue 可以快速比较新旧虚拟节点的差异,从而更高效地进行更新操作。 - 提升性能:使用正确的
key
可以显着提升列表渲染的性能。在没有key
的情况下,每次列表重新渲染时,整个列表的元素都会被销毁和重新创建,导致性能下降。而通过设置恰当的key
,框架可以识别哪些元素发生了变化,并且只更新变化的部分,减少不必要的操作。 - 维护组件状态:在列表中添加
key
后,React 和 Vue 将会确保带有相同key
值的元素始终保持相同的身份。这意味着即使列表项的顺序发生变化,带有相同key
的元素也不会被重新创建,而是保留其之前的状态。这对于在列表中操作表单元素或其他具有用户交互的组件时非常重要。
需要注意的是,key
属性必须是稳定且唯一的标识符,通常可以使用列表项的唯一标识符(如 id
),或者使用索引作为 key
(不推荐,除非列表项目没有唯一标识符)。
总结起来,key
属性在 React 和 Vue 项目中用于标识列表项的唯一性,它在虚拟 DOM 的更新过程中起到了重要的作用,可以提升性能、维护组件状态,并帮助框架准确识别和更新特定的组件。
⭐vuex面经
参考别人的博客Vuex面试题汇总 - 掘金 (juejin.cn)
⭐在vue上挂载全局属性
一般会挂载到Vue.prototype上
⭐如何定义vue插件
在 Vue.js 中,你可以通过编写插件来扩展 Vue 的功能。一个插件通常包含了一个对象或函数,其中包含了需要注册的组件、指令、过滤器、混入等。
下面是两种常见的方式来定义 Vue 插件:
1、对象形式的插件:
javascript
const myPlugin = {
install(Vue) {
// 在这里注册你的组件、指令、过滤器等
Vue.component('my-component', MyComponent);
Vue.directive('my-directive', MyDirective);
Vue.filter('my-filter', MyFilter);
// 添加全局方法或属性
Vue.prototype.$myMethod = function() {
// ...
};
}
};
// 在 Vue 实例中使用插件
Vue.use(myPlugin);
2、函数形式的插件:
php
const myPlugin = function(Vue) {
// 在这里注册你的组件、指令、过滤器等
Vue.component('my-component', MyComponent);
Vue.directive('my-directive', MyDirective);
Vue.filter('my-filter', MyFilter);
// 添加全局方法或属性
Vue.prototype.$myMethod = function() {
// ...
};
};
// 在 Vue 实例中使用插件
Vue.use(myPlugin);
无论你选择使用对象形式还是函数形式,都需要在 install
方法中完成对应组件、指令、过滤器的注册,并可以通过 Vue.prototype
添加全局方法或属性。然后使用 Vue.use()
方法来安装插件。
值得注意的是,插件必须在创建 Vue 实例之前被安装。通常,我们会在入口文件中全局注册插件。
通过编写和使用插件,我们可以很方便地扩展 Vue 的功能,并在多个组件中共享和复用这些功能。
shell
### ⭐vue上的一些属性
$options
对象上存储了 Vue 实例或组件的选项信息,包括通过混入(mixin)或直接定义在组件中的选项。下面是 $options
常见的一些属性:
components
:定义了当前组件注册的子组件。computed
:包含计算属性的定义。data
:组件的数据对象,在 Vue 实例创建时会对其进行初始化。methods
:包含组件中定义的方法。watch
:包含监听器的定义,用于监听指定数据的变化。filters
:包含过滤器的定义,用于对输出进行格式化处理。directives
:指令的注册对象,用于自定义指令。mixins
:数组,包含混入对象的选项。props
:定义组件的属性,可以是数组形式或对象形式。el
:指定 Vue 实例挂载的元素。template
:组件的模板字符串或模板编译生成的渲染函数。render
:渲染函数,用于自定义渲染组件的方式。
除了上述常见的属性,还可能包含其他自定义的选项,这取决于你在组件定义中添加的内容。
需要注意的是,$options
中的属性是在 Vue 实例或组件创建时解析和合并的,它们代表了最终被执行的选项。在 Vue 实例或组件的生命周期中,可以通过访问 $options
对象来查看和使用这些选项。
除了 options
属性,Vue.js 实例上还有一些其他属性。以下是一些常用的属性:
$el
:当前 Vue 实例关联的 DOM 元素。$data
:Vue 实例的数据对象。$refs
:包含了所有拥有ref
注册的子组件以及 DOM 元素的引用。$parent
:父级 Vue 实例。$root
:根 Vue 实例。$children
:当前 Vue 实例的直接子组件。$slots
:包含了所有插槽内容的对象。$scopedSlots
:包含了所有作用域插槽的对象。$attrs
:包含了父级作用域中不被 prop 所识别(且获取)的特性绑定(class 和 style 除外)。$listeners
:包含了父级作用域中的 (不含.native
修饰器的) v-on 事件监听器。
这些属性都是在 Vue 实例创建时动态添加到实例上的,可以随时读写和访问。利用这些属性,我们可以在 Vue 实例中获取它关联的 DOM 元素、数据对象、子组件等信息,并进行相应的操作。
⭐vue中的context属性
在 Vue 组件中,每个组件实例都会有自己独立的 context
对象,其中包括了包含了组件实例、父组件实例、子组件实例、props、slots、emit 等信息,会作为参数传递给组件的生命周期钩子函数或其他回调函数。
xml
<template>
<div>
<button @click="handleClick">点击按钮</button>
</div>
</template>
<script>
export default {
methods: {
handleClick() {
// 在这里访问 context 对象
console.log(this.$context);
}
}
}
</script>
⭐vue实例挂载过程
在 Vue 实例挂载过程中,发生了以下几个主要的步骤:
-
创建 Vue 实例:通过
new Vue()
创建一个 Vue 实例,并传入一个配置对象。 -
初始化阶段:
- 初始化实例成员:将配置对象中的数据和方法挂载到 Vue 实例上,同时创建实例的响应式数据。
- 调用
beforeCreate
钩子函数:在实例初始化之后,但是在数据观测和事件配置之前。 - 注入注入器(Injector):如果实例配置了 provide 选项,则会创建一个注入器,使得组件可以使用 inject 选项来注入依赖。
- 初始化事件:对事件进行初始化,包括调用
events
钩子函数、自动监听子组件的事件等。 - 调用
created
钩子函数:在实例创建完成之后调用,此时实例已经完成了数据观测,但尚未挂载到 DOM 上。
-
模板编译:
- 解析模板:对 Vue 模板进行编译,将模板解析为 Virtual DOM。
- 创建渲染函数:通过编译后的模板生成渲染函数,用于渲染组件的 Virtual DOM。
-
挂载阶段:
- 调用
beforeMount
钩子函数:在挂载开始之前被调用,此时 Virtual DOM 已经创建完成。 - 创建真实 DOM:将组件的 Virtual DOM 转换为真实 DOM,并插入到页面中的指定位置。
- 更新组件和子组件:根据数据的变化,对组件进行更新。
- 调用
mounted
钩子函数:在实例挂载之后调用,此时组件已经被挂载到 DOM 上。
- 调用
-
更新阶段:
- 数据更新:当数据发生变化时,Vue 会检测到变化并触发更新。
- 调用
beforeUpdate
钩子函数:在更新开始之前被调用,此时数据已经更新完成,但尚未重新渲染。 - 重新渲染:根据数据的变化,重新渲染组件。
- 调用
updated
钩子函数:在更新完成之后调用,此时组件已经重新渲染。
-
销毁阶段:
- 调用
beforeDestroy
钩子函数:在实例销毁之前调用。 - 解绑事件监听器:移除所有的事件监听器。
- 销毁子组件:递归销毁子组件。
- 调用
destroyed
钩子函数:在实例销毁之后调用。
- 调用
-
最终销毁:当 Vue 实例不再需要时,可以通过调用
$destroy()
方法手动销毁实例。
通过以上步骤,Vue 实例在挂载过程中完成了初始化、模板编译、挂载、更新和销毁等一系列的操作,从而实现了数据的双向绑定和组件的动态渲染。
⭐说说vue注入器
注入器(Injector)是一种机制,用于在组件树中向下传递数据或共享实例,使得组件可以通过配置的方式访问到所需的依赖对象。
注入器的配置通过在父组件中使用 provide
选项来进行设置,而子组件可以使用 inject
选项来注入所需的依赖。
例:
javascript
// 父组件提供依赖对象
const ParentComponent = {
provide: {
theme: 'dark', // 提供一个基本数据类型的依赖
apiService: new ApiService() // 提供一个实例依赖
},
// ...
};
// 子组件注入依赖对象
const ChildComponent = {
inject: ['theme', 'apiService'], // 注入 theme 和 apiService
created() {
console.log(this.theme); // 访问父组件提供的 theme
this.apiService.getData(); // 使用父组件提供的 apiService
},
// ...
};
在上述例子中,父组件通过 provide
选项提供了两个依赖对象:theme
和 apiService
。其中,theme
是一个基本数据类型的依赖,而 apiService
是一个实例依赖。
子组件通过 inject
选项注入了这两个依赖对象,并在 created
生命周期钩子函数中使用这些依赖。通过注入器的机制,子组件可以方便地访问并使用父组件提供的依赖对象。
需要注意的是,注入器是一个向下传递数据的机制,只能在父组件和其子组件之间进行传递。子组件可以通过 inject
选项指定需要注入的依赖,但是这些依赖只会在上级组件中查找,如果没有找到则会返回默认值或者 undefined
。同时,注入器是通过组件实例的引用来完成的,因此在组件树的不同位置会产生不同的实例。
⭐vue插件的作用
- 添加全局功能:插件可以在 Vue 实例上添加全局方法或属性,使其在整个应用中可用。例如,通过插件可以添加一个全局的
$http
方法,用于发起 AJAX 请求。这样,在任何组件中都可以直接使用$http
方法进行数据请求。 - 注册全局组件:插件可以注册全局组件,使得这些组件在任何地方都可以使用。这对于需要在多个组件中重复使用的组件非常有用,可以避免在每个组件中单独导入和注册。
- 注册全局指令:插件可以注册全局指令,使得这些指令在整个应用中都可用。全局指令可以用于操纵 DOM 元素、监听事件、修改样式等操作。
- 注册全局过滤器:插件可以注册全局过滤器,使得这些过滤器在整个应用中都可用。全局过滤器可以对数据进行格式化、处理和转换,方便在模板中使用。
- 添加实例方法:插件可以向 Vue.prototype 上添加实例方法,使得这些方法在每个 Vue 实例中都可用。这样,在组件中可以直接通过
this
访问这些方法,方便在组件中共享和调用。 - 添加混入(Mixin):插件可以添加全局混入对象,将其混入到组件的选项中。通过混入,可以在多个组件中共享相同的逻辑、方法和生命周期钩子函数。
⭐vue插件的定义与注册
vue
插件的实现应该暴露一个 install
方法。这个方法的第一个参数是 Vue
构造器,第二个参数是一个可选的选项对象
javascript
const MyPlugin = {}
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或 property
Vue.myGlobalMethod = function () {
// 逻辑...
}
// 2. 添加全局资源
/*
这段代码是在 Vue 中定义了一个全局指令,名称为 'my-directive'。通过使用 Vue.directive() 方法,可以注册全局指令并为其指定相应的配置对象。
在这个例子中,配置对象包含 bind 函数,它是指令的钩子函数之一。当该指令绑定到元素时,bind 函数会被调用,并且会传入以下参数:
el:指令所绑定的元素,可以用于直接操作 DOM。
binding:一个对象,包含以下属性:
name:指令的名称,即 'my-directive'。
value:指令的绑定值,可以是静态值或动态绑定的表达式。
oldValue:上一个绑定值,仅在组件更新时才可用。
arg:指令的参数,如果有的话。
modifiers:一个包含修饰符的对象。
vnode:Vue 编译生成的虚拟节点。
oldVnode:上一个虚拟节点,仅在组件更新时才可用。
*/
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
...
})
// 3. 注入组件选项
Vue.mixin({
created: function () {
// 逻辑...
}
...
})
// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}
插件的注册通过Vue.use()
的方式进行注册(安装),第一个参数为插件的名字,第二个参数是可选择的配置项
less
Vue.use(你的插件,{ /* ... */} )
注意的是:
注册插件的时候,需要在调用 new Vue()
启动应用之前完成
Vue.use
会自动阻止多次注册相同插件,只会注册一次
总结:插件 (Plugin)
是用来增强你的技术栈的功能模块,它的目标是 Vue
本身。简单来说,插件就是指对Vue
的功能的增强或补充
shell
### ⭐说一说mixin
在 Vue 中,mixin
是一种重用组件选项的方式。它允许将公共的逻辑、属性和方法提取出来,并混入到多个组件中。
使用方式:
- 创建一个 mixin 对象,包含要混入的选项,例如
data
、methods
、computed
、watch
和生命周期钩子。 - 在组件中使用
mixins
选项将 mixin 对象混入。
下面是一个示例:
javascript
<!-- myMixin.js -->
export default {
data() {
return {
counter: 0
};
},
methods: {
increment() {
this.counter++;
}
},
computed: {
doubledCounter() {
return this.counter * 2;
}
},
watch: {
counter(newValue) {
console.log('Counter changed:', newValue);
}
},
created() {
console.log('Mixin created');
}
};
xml
<!-- MyComponent.vue -->
<template>
<div>
<p>Counter: {{ counter }}</p>
<p>Doubled Counter: {{ doubledCounter }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script>
import myMixin from './myMixin.js';
export default {
mixins: [myMixin],
created() {
console.log('Component created');
}
};
</script>
在上述示例中,myMixin.js
定义了一个 mixin 对象,其中包含了 data
、methods
、computed
、watch
和 created
生命周期钩子。然后,我们通过在组件的 mixins
选项中指定这个 mixin 对象,将其混入到组件中。
在组件 MyComponent.vue
中,我们可以直接使用混入的 data
、methods
、computed
和 watch
。同时,组件的生命周期钩子和 mixin 的生命周期钩子都会被调用。
当 mixin 和组件具有相同的选项时,Vue 会通过一定规则进行合并:
- 替换型策略:
props
、methods
、inject
和computed
。当 mixin 和组件具有相同的选项时,组件会覆盖 mixin 的选项。这意味着组件提供的选项会取代 mixin 中的选项。 - 合并型策略:
data
。当 mixin 和组件都有data
选项时,Vue 会通过调用Object.assign()
将两个对象合并为一个新的对象。这样做的目的是确保组件和 mixin 都可以访问到各自的data
。如果两个对象中有相同的属性,组件的值将覆盖 mixin 的值。 - 队列型策略:生命周期函数(如
created
、mounted
等)和watch
。当 mixin 和组件具有相同的生命周期钩子或watch
监听器时,它们都会被调用。具体的执行顺序是,首先执行 mixin 的钩子或监听器,然后再执行组件的钩子或监听器。这样可以确保 mixin 和组件都能正常运行,并且 mixin 的逻辑与组件的逻辑能够顺序执行。 - 叠加型策略:
components
、directives
和filters
。这些选项是通过原型链进行叠加的,意味着组件和 mixin 都可以访问到它们。如果 mixin 和组件具有相同名称的组件、指令或过滤器,它们会被叠加在一起,组成一个新的数组。这样做的目的是将 mixin 和组件的功能进行组合,使它们共同生效。
⭐说说vue中常用的修饰符
事件修饰符:
-
.prevent
:阻止默认事件- 应用场景:在表单提交时阻止默认刷新页面的行为。
xml<form @submit.prevent="handleSubmit"> <!-- 表单内容 --> </form>
-
.stop
:停止事件冒泡- 应用场景:停止事件冒泡,防止事件从当前元素向父元素传播。它可以阻止事件传递给更上层的元素,只在当前元素上执行绑定的事件处理函数。
xml<div @click="handleDivClick"> <!--只会执行button上的方法--> <button @click.stop="handleButtonClick">按钮</button> </div>
-
.once
:只触发一次事件- 应用场景:确保事件只会被触发一次,之后不再响应。
ini<button @click.once="handleButtonClick">点击一次</button>
-
.capture
:使用捕获模式监听事件- 应用场景:从外至内触发事件。
ini<div @click.capture="handleDivClick"> <button @click="handleButtonClick">按钮</button> </div>
-
.self
:只在事件目标自身触发时才执行- 应用场景:仅当事件发生在元素自身时才执行相应的处理函数。
ini<div @click.self="handleDivClick"> <button @click="handleButtonClick">按钮</button> </div>
-
.keyCode
:监听特定按键触发事件- 应用场景:根据按下的键盘按键来触发特定的事件。
ini<input @keydown.enter="handleEnterKey">
-
.passive
:当我们在监听元素滚动事件的时候,会一直触发onscroll
事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll
事件使用了一个.lazy
修饰符xml<div @scroll.passive="handleScroll"> <!-- 内容 --> </div>
在上述示例中,
@scroll.passive
表示监听div
元素的滚动事件,并使用.passive
修饰符。这样在滚动过程中,浏览器将以非阻塞的方式执行事件处理函数handleScroll
,提高页面的滚动性能。需要注意的是,
.passive
修饰符只在支持addEventListener
第三个参数选项的浏览器中生效。对于不支持的浏览器,.passive
修饰符将被忽略,事件仍然以常规方式进行处理。 -
.native
:使用native修饰符我们可以为自定义组件绑定原生事件vbnet<custom-component v-on:click.native="handleClick"></custom-component>
如果不使用native,这样绑定的点击事件就无法触发,因为custom-component本质上并不是一个dom,只能通过自定义事件的形式为子组件绑定dom
鼠标按钮修饰符:
- left 左键点击
- right 右键点击
- middle 中键点击
ini
<button @click.left="handleClick">ok</button>
<button @click.right="handleClick">ok</button>
<button @click.middle="handleClick">ok</button>
键盘修饰符:
键盘修饰符是用来修饰键盘事件(onkeyup
,onkeydown
)的,有如下:
keyCode
存在很多,但vue
为我们提供了别名,分为以下两种:
- 普通键(enter、tab、delete、space、esc、up...)
- 系统修饰键(ctrl、alt、meta、shift...)
typescript
// 只有按键为keyCode的时候才触发
<input type="text" @keyup.keyCode="handleKeydown">
还可以通过以下方式自定义一些全局的键盘码别名
ini
Vue.config.keyCodes.f2 = 113
表单修饰符:
1、.lazy
:默认情况下,在 input
、textarea
和 select
元素上使用 v-model
会在每次输入时更新绑定的数据。通过添加 .lazy
修饰符,可以改为在失去焦点或按下回车键时再更新数据(原理是从监听input事件转为监听onchange事件)。
ini
<input v-model.lazy="message" />
2、.number
:将用户输入值转换为数字类型。如果使用 v-model
绑定一个 input
元素,用户输入的内容将自动被转换为数字类型。如果无法转换,则不转换。
ini
<input v-model.number="age" type="number" />
3、.trim
:自动去除用户输入的首尾空格。
ini
<input v-model.trim="username" type="text" />
4、.prevent
:阻止默认的表单提交行为,防止提交时刷新页面。通常用于在提交表单之前进行验证或处理后再提交。
xml
<form @submit.prevent="submitForm">
<!-- 表单内容 -->
<button type="submit">提交</button>
</form>
v-bind修饰符:
-
.prop
修饰符:ini<child-component v-bind:child-prop.prop="parentData"></child-component>
在这个示例中,
.prop
修饰符用于绑定父组件的parentData
数据到子组件的child-prop
prop 属性上。 -
.camel
修饰符:perl<my-component v-bind:my-attribute.camel="value"></my-component>
使用
.camel
修饰符时,v-bind
将自动将my-attribute
转换为驼峰命名的属性名,然后绑定到组件上。例如绑定到myComponent
组件的myAttribute
属性。 -
.sync
修饰符:ini<custom-input v-bind:value.sync="inputValue"></custom-input>
使用
.sync
修饰符时,数据的双向绑定会更加方便。在这个示例中,custom-input
组件接收来自父组件的inputValue
数据,并且可以修改该值。任何对inputValue
的修改都会反映到父组件的数据中。 -
.once
修饰符:ini<div v-bind:id.once="dynamicId"></div>
使用
.once
修饰符时,绑定的值只会被解析一次,不会随后续数据变化而更新。这在某些情况下可以提高性能。
另外,v-bind 还可以通过 JavaScript 表达式动态绑定多个属性,例如:
css
<div v-bind="{ id: dynamicId, class: dynamicClass }"></div>
⭐vuex的使用流程
vuex本质是一个插件,下载引入后通过Vue.use方法进行注册,最终new Vue的时候将其作为配置项传入,之后全局就可以通过Vue原型上的$store属性获取到vuex实例
ini
new Vue({
store,
render: h => h(App)
}).$mount('#app')
配置好后我们通过Vuex.store创建Vuex store实例,在其中进行配置
-
state:数据仓库,存放数据
-
getters:可以获取state中的属性,做处理后返回,同时产生缓存,下一次调用时若其中依赖项没有更新,则返回缓存值,与计算属性类似
-
mutation:在其中修改state中的属性,在组件中通过$store.commit('mutationName')进行提交修改
-
actions:在其中进行异步任务处理,在组件中通过$store.dispatch('actionsName')进行提交,actions中的方法会默认传入context,我们一般将commit解构出来,待异步任务完成后,调用commit通知mutation进行数据修改
-
modules:在数据变多,vuex store配置变得臃肿时,就可以通过分模块的方式将相同功能逻辑的数据单独定义成模块,在提交commit或dispatch时,要加上模块路径:
$store.commit('moduleA/mutationName')
cssconst moduleA = { state: { ... }, mutations: { ... }, actions: { ... }, getters: { ... } }; const moduleB = { state: { ... }, mutations: { ... }, actions: { ... }, getters: { ... } }; const store = new Vuex.Store({ modules: { moduleA, moduleB } });
⭐说一说mixin
在 Vue 中,mixin
是一种重用组件选项的方式。它允许将公共的逻辑、属性和方法提取出来,并混入到多个组件中。
使用方式:
- 创建一个 mixin 对象,包含要混入的选项,例如
data
、methods
、computed
、watch
和生命周期钩子。 - 在组件中使用
mixins
选项将 mixin 对象混入。
下面是一个示例:
javascript
<!-- myMixin.js -->
export default {
data() {
return {
counter: 0
};
},
methods: {
increment() {
this.counter++;
}
},
computed: {
doubledCounter() {
return this.counter * 2;
}
},
watch: {
counter(newValue) {
console.log('Counter changed:', newValue);
}
},
created() {
console.log('Mixin created');
}
};
xml
<!-- MyComponent.vue -->
<template>
<div>
<p>Counter: {{ counter }}</p>
<p>Doubled Counter: {{ doubledCounter }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script>
import myMixin from './myMixin.js';
export default {
mixins: [myMixin],
created() {
console.log('Component created');
}
};
</script>
在上述示例中,myMixin.js
定义了一个 mixin 对象,其中包含了 data
、methods
、computed
、watch
和 created
生命周期钩子。然后,我们通过在组件的 mixins
选项中指定这个 mixin 对象,将其混入到组件中。
在组件 MyComponent.vue
中,我们可以直接使用混入的 data
、methods
、computed
和 watch
。同时,组件的生命周期钩子和 mixin 的生命周期钩子都会被调用。
当 mixin 和组件具有相同的选项时,Vue 会通过一定规则进行合并:
- 替换型策略:
props
、methods
、inject
和computed
。当 mixin 和组件具有相同的选项时,组件会覆盖 mixin 的选项。这意味着组件提供的选项会取代 mixin 中的选项。 - 合并型策略:
data
。当 mixin 和组件都有data
选项时,Vue 会通过调用Object.assign()
将两个对象合并为一个新的对象。这样做的目的是确保组件和 mixin 都可以访问到各自的data
。如果两个对象中有相同的属性,组件的值将覆盖 mixin 的值。 - 队列型策略:生命周期函数(如
created
、mounted
等)和watch
。当 mixin 和组件具有相同的生命周期钩子或watch
监听器时,它们都会被调用。具体的执行顺序是,首先执行 mixin 的钩子或监听器,然后再执行组件的钩子或监听器。这样可以确保 mixin 和组件都能正常运行,并且 mixin 的逻辑与组件的逻辑能够顺序执行。 - 叠加型策略:
components
、directives
和filters
。这些选项是通过原型链进行叠加的,意味着组件和 mixin 都可以访问到它们。如果 mixin 和组件具有相同名称的组件、指令或过滤器,它们会被叠加在一起,组成一个新的数组。这样做的目的是将 mixin 和组件的功能进行组合,使它们共同生效。
⭐说说vue中常用的修饰符
事件修饰符:
-
.prevent
:阻止默认事件- 应用场景:在表单提交时阻止默认刷新页面的行为。
xml<form @submit.prevent="handleSubmit"> <!-- 表单内容 --> </form>
-
.stop
:停止事件冒泡- 应用场景:停止事件冒泡,防止事件从当前元素向父元素传播。它可以阻止事件传递给更上层的元素,只在当前元素上执行绑定的事件处理函数。
xml<div @click="handleDivClick"> <!--只会执行button上的方法--> <button @click.stop="handleButtonClick">按钮</button> </div>
-
.once
:只触发一次事件- 应用场景:确保事件只会被触发一次,之后不再响应。
ini<button @click.once="handleButtonClick">点击一次</button>
-
.capture
:使用捕获模式监听事件- 应用场景:从外至内触发事件。
ini<div @click.capture="handleDivClick"> <button @click="handleButtonClick">按钮</button> </div>
-
.self
:只在事件目标自身触发时才执行- 应用场景:仅当事件发生在元素自身时才执行相应的处理函数。
ini<div @click.self="handleDivClick"> <button @click="handleButtonClick">按钮</button> </div>
-
.keyCode
:监听特定按键触发事件- 应用场景:根据按下的键盘按键来触发特定的事件。
ini<input @keydown.enter="handleEnterKey">
-
.passive
:当我们在监听元素滚动事件的时候,会一直触发onscroll
事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll
事件使用了一个.lazy
修饰符xml<div @scroll.passive="handleScroll"> <!-- 内容 --> </div>
在上述示例中,
@scroll.passive
表示监听div
元素的滚动事件,并使用.passive
修饰符。这样在滚动过程中,浏览器将以非阻塞的方式执行事件处理函数handleScroll
,提高页面的滚动性能。需要注意的是,
.passive
修饰符只在支持addEventListener
第三个参数选项的浏览器中生效。对于不支持的浏览器,.passive
修饰符将被忽略,事件仍然以常规方式进行处理。 -
.native
:使用native修饰符我们可以为自定义组件绑定原生事件vbnet<custom-component v-on:click.native="handleClick"></custom-component>
如果不使用native,这样绑定的点击事件就无法触发,因为custom-component本质上并不是一个dom,只能通过自定义事件的形式为子组件绑定dom
鼠标按钮修饰符:
- left 左键点击
- right 右键点击
- middle 中键点击
ini
<button @click.left="handleClick">ok</button>
<button @click.right="handleClick">ok</button>
<button @click.middle="handleClick">ok</button>
键盘修饰符:
键盘修饰符是用来修饰键盘事件(onkeyup
,onkeydown
)的,有如下:
keyCode
存在很多,但vue
为我们提供了别名,分为以下两种:
- 普通键(enter、tab、delete、space、esc、up...)
- 系统修饰键(ctrl、alt、meta、shift...)
typescript
// 只有按键为keyCode的时候才触发
<input type="text" @keyup.keyCode="handleKeydown">
还可以通过以下方式自定义一些全局的键盘码别名
ini
Vue.config.keyCodes.f2 = 113
表单修饰符:
1、.lazy
:默认情况下,在 input
、textarea
和 select
元素上使用 v-model
会在每次输入时更新绑定的数据。通过添加 .lazy
修饰符,可以改为在失去焦点或按下回车键时再更新数据(原理是从监听input事件转为监听onchange事件)。
ini
<input v-model.lazy="message" />
2、.number
:将用户输入值转换为数字类型。如果使用 v-model
绑定一个 input
元素,用户输入的内容将自动被转换为数字类型。如果无法转换,则不转换。
ini
<input v-model.number="age" type="number" />
3、.trim
:自动去除用户输入的首尾空格。
ini
<input v-model.trim="username" type="text" />
4、.prevent
:阻止默认的表单提交行为,防止提交时刷新页面。通常用于在提交表单之前进行验证或处理后再提交。
xml
<form @submit.prevent="submitForm">
<!-- 表单内容 -->
<button type="submit">提交</button>
</form>
v-bind修饰符:
-
.prop
修饰符:ini<child-component v-bind:child-prop.prop="parentData"></child-component>
在这个示例中,
.prop
修饰符用于绑定父组件的parentData
数据到子组件的child-prop
prop 属性上。 -
.camel
修饰符:perl<my-component v-bind:my-attribute.camel="value"></my-component>
使用
.camel
修饰符时,v-bind
将自动将my-attribute
转换为驼峰命名的属性名,然后绑定到组件上。例如绑定到myComponent
组件的myAttribute
属性。 -
.sync
修饰符:ini<custom-input v-bind:value.sync="inputValue"></custom-input>
使用
.sync
修饰符时,数据的双向绑定会更加方便。在这个示例中,custom-input
组件接收来自父组件的inputValue
数据,并且可以修改该值。任何对inputValue
的修改都会反映到父组件的数据中。 -
.once
修饰符:ini<div v-bind:id.once="dynamicId"></div>
使用
.once
修饰符时,绑定的值只会被解析一次,不会随后续数据变化而更新。这在某些情况下可以提高性能。
另外,v-bind 还可以通过 JavaScript 表达式动态绑定多个属性,例如:
css
<div v-bind="{ id: dynamicId, class: dynamicClass }"></div>
⭐vuex的使用流程
vuex本质是一个插件,下载引入后通过Vue.use方法进行注册,最终new Vue的时候将其作为配置项传入,之后全局就可以通过Vue原型上的$store属性获取到vuex实例
ini
new Vue({
store,
render: h => h(App)
}).$mount('#app')
配置好后我们通过Vuex.store创建Vuex store实例,在其中进行配置
-
state:数据仓库,存放数据
-
getters:可以获取state中的属性,做处理后返回,同时产生缓存,下一次调用时若其中依赖项没有更新,则返回缓存值,与计算属性类似
-
mutation:在其中修改state中的属性,在组件中通过$store.commit('mutationName')进行提交修改
-
actions:在其中进行异步任务处理,在组件中通过$store.dispatch('actionsName')进行提交,actions中的方法会默认传入context,我们一般将commit解构出来,待异步任务完成后,调用commit通知mutation进行数据修改
-
modules:在数据变多,vuex store配置变得臃肿时,就可以通过分模块的方式将相同功能逻辑的数据单独定义成模块,在提交commit或dispatch时,要加上模块路径:
$store.commit('moduleA/mutationName')
cssconst moduleA = { state: { ... }, mutations: { ... }, actions: { ... }, getters: { ... } }; const moduleB = { state: { ... }, mutations: { ... }, actions: { ... }, getters: { ... } }; const store = new Vuex.Store({ modules: { moduleA, moduleB } });
⭐说一说vue2中nextTick的用途及其原理
vue2采用了异步更新DOM的策略,DOM的更新操作会被放至微任务队列中执行,这就意味着如果在修改数据后立马获取相应的DOM,我们获取到的其实是旧DOM,那么如何获取新DOM呢?思路就是同样也将获取新DOM的操作放到微队列或宏队列中,nextTick就是这样做的。
调用nextTick后,内部会判断当前浏览器是否支持Promise,若不支持,则会使用宏任务来实现异步,依次检测是否支持setImmediate、MessageChannel,最终使用setTimeout保底。
若在一次事件循环中多次调用nextTick,vue不会推送多个nextTick任务到队列中,而是会将所有回调都保存在一个回调数组中,下一次事件循环时遍历执行
PS: <math xmlns="http://www.w3.org/1998/Math/MathML"> n e x t T i c k 和 n e x t T i c k 是同一原理, nextTick和nextTick是同一原理, </math>nextTick和nextTick是同一原理,nextTick中调用的就是nextTick
⭐说说$set
$set可以让我们通过key来设置数组或对象上的属性,并确保其可以被转换成响应式属性。
我们之前说过了响应式原理,如果还不清楚的请先去了解vue2响应式:小何的世界 (theluckyone.top)
先说 <math xmlns="http://www.w3.org/1998/Math/MathML"> s e t ,用法: ' v m . set,用法:`vm. </math>set,用法:'vm.set(target, key, value)`
对于数组: 当target为数组时,内部会先判断key与数组长度length的大小
- 如果key大于数组长度,那么就要先修改数组length = key,然后通过在拦截器中重写过的splice方法将value插入到对应位置,这样在splice方法中,数组拦截器就会侦测到target的变化,并自动帮我们把这个新增的val转换成响应式的。
- 如果key小于数组长度,那么这个key已经被侦测了变化,直接
target[key] = val
修改就好了
对于对象:
- 首先先要获取对象上的__ob__属性,也就是Observer实例,在上面的文章中我们介绍过这个对象代表着对象已经被响应式监听
- 再判断该对象是不是Vue实例(通过target._isVue来判断)或Vue实例的根数据对象(通过ob.vmCount来判断),如果是则不予处理
- 如果不是响应式数据的情况(没有__ob__属性),就直接target.key = val,不需要多余处理
- 最后处理响应式数据的情况,这种情况我们需要跟踪新增属性的变化,使用defineReactive为其定义getter/setter,最后执行target对应的依赖容器中notify方法触发变化通知
⭐说说$delete
$delete用于删除响应式数据中的某个属性,原理很简单,分数组和对象:
- 删除对象上的属性:获取target上的__ob__实例,通过delete关键字删除后,调用__ob__.dep.notify()来通知watcher数据发生了变化
- 删除数组上的值:使用splice将key指定的索引位置的元素删除即可,数组拦截器会自动向依赖发送通知
PS:与 <math xmlns="http://www.w3.org/1998/Math/MathML"> s e t 一样, set一样, </math>set一样,delete同样不可以在Vue实例或Vue实例的根数据对象上使用
⭐Vue初始化时做了什么事
源码流程:
scss
initLifeCircle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initState(vm);
callHook(vm, 'created');
if(vm.$options.el) {
vm.$mount(vm.$options.el)
}
⭐v-model的原理
v-model其实是v-bind和v-on的语法糖,v-bind用于绑定input的value,v-on用于绑定用户输入事件,这样用户输入时,data中绑定的inputValue就会改,而data中的inputValue改变时,v-bind就会生效,将改变后的值自动传递给输入框
更深层的原理呢?
v-on对输入框绑定的一般是input事件,在每次输入时就会触发,如果像让用户输入完数据点击外部,输入框失去焦点之后再触发数据更新,就可以改为用change事件,这也是v-model.lazy
的原理(补充:单选框和复选框一般直接是用change事件的)
v-bind实现了数据驱动视图,data中数据改变,input框内数据也会改变(如果是单选框和复选框就是勾选状态变化),这是通过vue响应式来实现的,vue2采用defineProperty劫持对象,采用观察者模式进行依赖收集以及依赖派发来实现,具体请移步:小何的世界 (theluckyone.top)
⭐说说ref和reactive
ref 本质上是一个"包裹对象"。因为JavaScript的Proxy 无法提供对原始值的代理,所以我们需要使用一层对象作为包裹,间接实现原始值的响应。
在ref实现流程中,如果不传入第二个参数isShallow,那么默认会为其包裹一层对象后调用toReactive转换为响应式对象
ini
this._value = __v_isShallow ? value : toReactive(value);
而在RefImpl类中,也为_value定义了getter和setter,在getter中收集依赖,在setter中响应式地修改value
kotlin
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
}
}
reactive一般用于引用数据类型的响应式,利用proxy来实现响应式逻辑,与ref不同,不需要使用.value来进行数据的获取,起初只是为了方便vue2用户的习惯而添加,但现在官网也推荐统一使用ref来添加响应式
总的来说,ref(val)可以类比于reactive({value: val})
⭐Vue中的name有什么作用
-
在组件内部的模板中可以通过name来调用自己,称为递归组件
-
移除keep-alive中不需要缓存的组件,使用
exclude="name"
-
vue-devtools中显示的是组件的名称
-
动态组件中,使用name来绑定对应的组件
xml<template> <div id='app'> <component :is='textCompName'></component> </div> </template> <script>import textComp from './components/TextComp' data(){ return{ textCompName:'TextComp' } } </script>
⭐路由守卫有哪些?
- 全局前置守卫 (
beforeEach
): 在路由切换开始之前调用,可以用来进行全局的身份验证、权限控制等。使用router.beforeEach
注册全局前置守卫。 - 全局解析守卫 (
beforeResolve
): 会在路由切换开始之前先被调用,也就是在组件被实例化之前调用。使用router.beforeResolve
注册全局解析守卫。 - 全局后置钩子 (
afterEach
): 在路由切换完成之后调用,可以用来进行一些统计、页面滚动等操作。使用router.afterEach
注册全局后置钩子。 - 路由独享守卫 (
beforeEnter
): 在路由配置中直接定义的守卫,仅作用于当前路由。在路由配置对象中通过beforeEnter
字段定义路由独享守卫。 - 组件内的守卫 (
beforeRouteEnter
、beforeRouteUpdate
、beforeRouteLeave
): 在组件内部定义的守卫,用于对当前组件的路由进行控制。beforeRouteEnter
在进入组件之前调用,beforeRouteUpdate
在组件复用时调用,beforeRouteLeave
在离开组件时调用。
⭐对vue项目的优化
(1)代码层面的优化
- v-if 和 v-show 区分使用场景
- computed 和 watch 区分使用场景
- v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
- 长列表性能优化
- 事件的销毁
- 图片资源懒加载
- 路由懒加载
- 第三方插件的按需引入
- 优化无限列表性能
- 服务端渲染 SSR or 预渲染
(2)Webpack 层面的优化
- Webpack 对图片进行压缩
- 减少 ES6 转为 ES5 的冗余代码
- 提取公共代码
- 模板预编译
- 提取组件的 CSS
- 优化 SourceMap
- 构建结果输出分析
- Vue 项目的编译优化
(3)基础的 Web 技术的优化
- 开启 gzip 压缩
- 浏览器缓存
- CDN 的使用
- 使用 Chrome Performance 查找性能瓶颈
⭐Vuex和Pinia的区别
-
Pinia采用了去中心化的管理方式,每个模块都有自己的store,而Vuex则是使用module分模块处理
-
Pinia去掉了mutation,异步和同步统一由actions进行处理
-
Pinia对TS的支持更好
🌙React
⭐简要介绍一下 React Hooks,并举例说明它的用途和使用方式。
React Hooks 是 React v16.8 引入的一项功能,它可以让你在函数组件中使用状态(state)和其他 React 特性。传统的 React 类组件使用类来管理状态和生命周期方法,而 React Hooks 让你在无需编写类的情况下,能够使用状态和其他 React 的特性。
React Hooks 提供了一些预定义的钩子函数,例如 useState、useEffect、useContext 等。这些钩子函数允许你在函数组件中管理状态、进行副作用操作、访问全局的上下文等。使用 Hooks 可以使代码更简洁、可读性更好,并且方便进行复用和测试。
以下是几个常用的 React Hooks:
🌰useState:useState 钩子允许在函数组件中创建和管理状态。它返回一个包含当前状态和更新状态的函数的数组。可以通过解构赋值的方式获取和更新状态的值。
javascript
import React, { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
🌰useEffect:useEffect 钩子用于处理副作用操作,例如订阅事件、异步请求等。它在每次渲染完成后执行,并且可以通过返回一个清理函数来处理取消订阅或清理操作。
javascript
import React, { useEffect } from 'react';
function Example() {
useEffect(() => {
// 在组件加载和更新后执行副作用操作
console.log('Component rendered');
// 返回清理函数,在组件卸载前执行
return () => {
console.log('Component unmounted');
};
}, []);
return <div>Example Component</div>;
}
🌰useContext:useContext 钩子允许在函数组件中访问全局上下文。它接收一个 Context 对象作为参数,返回该 Context 的当前值。
javascript
import React, { useContext } from 'react';
import MyContext from './MyContext';
function Example() {
const value = useContext(MyContext);
return <div>Context Value: {value}</div>;
}
🌰Reducer:Reducer是一种状态管理方式,它通过一个纯函数来管理状态。该函数接收两个参数,一个是当前状态,另一个是触发状态变化的操作。Reducer会根据操作类型修改状态,并返回一个新的状态,而不直接对原状态进行修改。
在React中使用Reducer可以代替类组件中的setState方法,可以更好地控制状态更新的过程。使用Reducer可以将状态和操作分离处理,并且允许多个组件共享同一个状态对象。
javascript
import React, { useReducer } from 'react';
// 定义初始状态
const initialState = {
count: 0,
};
// 定义Reducer函数
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'decrement':
return { ...state, count: state.count - 1 };
default:
throw new Error('Unsupported action type');
}
};
const Counter = () => {
// 使用useReducer创建状态和操作
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
};
export default Counter;
🌰useMemo:useMemo是一个性能优化的钩子,它用于缓存计算结果以避免重复计算。useMemo接收两个参数,第一个是计算结果的函数,第二个是依赖项数组。只有当某个依赖项发生变化时,useMemo才会重新计算结果。否则直接返回缓存的结果。
在React中,由于组件的render()方法会在状态或属性变化时被调用,如果渲染过程中需要执行一些复杂计算,就会影响到页面性能。使用useMemo可以缓存这些计算结果,减少不必要的计算,提高应用性能。
typescript
import React, { useMemo } from 'react';
const ExpensiveCalculation = ({ number }) => {
// 使用useMemo缓存计算结果
const result = useMemo(() => {
console.log('Expensive calculation'); // 仅在依赖项变化时触发
let sum = 0;
for (let i = 1; i <= number; i++) {
sum += i;
}
return sum;
}, [number]); // 仅当number变化时重新计算
return <div>Result: {result}</div>;
};
export default ExpensiveCalculation;
总而言之,React Hooks 是一种用于处理状态和其他 React 特性的方式,它可以让你在函数组件中做很多以前只能在类组件中实现的事情。通过使用 React Hooks,你可以编写更简洁、可维护的代码,并享受更好的开发体验。
⭐Redux的基本使用
-
调用createStore创建store,作为公共数据区域
-
定义ruducer纯函数,接收state和action,默认返回state
-
在组件中通过store.dispatch派发action,其中包含type和payload等值,我们通常将action封装为一个返回action对象的函数,在函数内做操作
-
reducer接收到action后通过switch根据type来判断要走哪个分支,返回深拷贝并进行操作后的state对象
-
在组件中通过subscribe来进行订阅,传入回调函数,store中数据修改就会进行回调通知,通过getState就可以获取到state中的数据
-
如果要实现异步操作或日志打印,就需要使用redux中间件redux-thunk/redux-promise和redux-logger来进行
- redux-thunk,允许dispatch中传入函数,在函数内部进行异步请求后返回action
- redux-promise,允许dispatch中传入promise对象
🌙TS
⭐简要说明 TypeScript 中的类型注解和类型推断的区别,并举例说明。
类型注解是在代码中明确指定变量的类型,使用冒号(:)后跟类型名称的方式进行注解。通过类型注解,开发者可以为变量、函数参数、函数返回值等明确指定类型。
例如:
typescript
let num: number = 10;
function add(a: number, b: number): number {
return a + b;
}
类型推断是 TypeScript 的一项特性,在变量声明时根据变量的初始值自动推导出变量的类型。通过类型推断,开发者可以省略变量的类型注解,让 TypeScript 根据上下文自动推断出变量的类型。
例如:
typescript
let num = 10; // 推断为 number 类型
function add(a: number, b: number) { // 推断返回值类型为 number
return a + b;
}
类型注解和类型推断的区别主要在于显式性和隐式性。类型注解更加明确,可以提高代码的可读性和可维护性,但需要开发者手动指定类型;类型推断更加隐式,可以减少代码的冗余,提高开发效率,但有时可能推断出的类型不符合预期。
⭐请解释 TypeScript 中的泛型,并举例说明其用途和使用方式。
泛型在 TypeScript 中的使用方式非常灵活,可以用于函数、类、接口等各种场景。通过使用泛型,我们可以在定义时不指定具体的类型,而是使用一个占位符(通常为单个大写字母,比如 T)来代表类型,使得代码具有更高的灵活性和重用性。
泛型的主要用途包括:
1、提供类型安全的容器:通过使用泛型,可以创建安全的数据容器,例如数组或集合类型,使其只能存储指定类型的元素。
ini
function toArray<T>(arg: T): T[] {
return [arg];
}
const arr = toArray<number>(10); // 创建存储数字类型的数组
2、增强代码的可扩展性:通过将类型参数化,可以编写更通用的代码,减少代码的重复和冗余,并且能够应对多种类型的需求。
php
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const mergedObj = merge({ name: 'Tom' }, { age: 20 }); // 合并两个对象的属性
3、可以在类或接口中使用泛型,实现更灵活的类型约束和抽象。
typescript
interface Container<T> {
value: T;
getValue(): T;
}
class NumberContainer implements Container<number> {
value: number;
constructor(value: number) {
this.value = value;
}
getValue(): number {
return this.value;
}
}
总结:泛型是 TypeScript 中非常强大和重要的特性,它能够增强代码的类型安全性和可扩展性。通过合理应用泛型,我们可以编写更灵活、通用、可维护的代码。
⭐请解释 TypeScript 中的装饰器是什么,以及如何应用装饰器。
装饰器是一种特殊类型的函数,可以用于修改类、属性或者方法的行为。装饰器通过在被修饰的目标周围包裹额外的逻辑来实现对目标进行扩展或者改变。
装饰器可以定义在类、属性和方法上,它们分别称为类装饰器、属性装饰器和方法装饰器。装饰器函数可以接收不同参数,参数的数量和类型取决于装饰器所应用的目标类型。
下面我们来逐个说明装饰器的定义,并举例装饰器分别定义在类、属性和方法上的情况:
-
类装饰器(Class Decorator):
-
定义:类装饰器是应用于类构造函数的函数。它接收一个参数,即被装饰的类的构造函数。
-
示例:
typescriptfunction logClass(target: any) { console.log(`Class ${target.name} is decorated`); } @logClass class MyClass { // 类的定义 }
在这个例子中,
logClass
是一个类装饰器,它被应用到MyClass
类上。当我们使用@logClass
语法将装饰器应用到类上时,装饰器函数会在类的构造函数上进行操作,这里会输出 "Class MyClass is decorated"。
-
-
属性装饰器(Property Decorator):
-
定义:属性装饰器是应用于类的属性的函数。它接收两个参数,分别是目标类的原型、属性名。
-
示例:
typescriptfunction logProperty(target: any, propertyKey: string) { console.log(`Property ${propertyKey} is decorated in class ${target.constructor.name}`); } class MyClass { @logProperty myProperty: string; }
在这个例子中,
logProperty
是一个属性装饰器,它被应用到MyClass
类的myProperty
属性上。装饰器函数会在属性的定义阶段进行操作,这里会输出 "Property myProperty is decorated in class MyClass"。
-
-
方法装饰器(Method Decorator):
-
定义:方法装饰器是应用于类的方法的函数。它接收三个参数,分别是目标类的原型、方法名和方法的属性描述符。
-
示例:
typescriptfunction logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) { console.log(`Method ${propertyKey} is decorated in class ${target.constructor.name}`); } class MyClass { @logMethod greet(name: string) { console.log(`Hello, ${name}!`); } }
在这个例子中,
logMethod
是一个方法装饰器,它被应用到MyClass
类的greet
方法上。装饰器函数在方法的定义阶段进行操作,这里会输出 "Method greet is decorated in class MyClass"。
-
通过使用装饰器,我们可以方便地扩展或者修改类、属性或者方法的行为,实现一些额外的逻辑或者功能。装饰器提供了一种优雅且灵活的方式来对代码进行元编程。
🌙NodeJs
Express
⭐如何使用Express开发一个服务端模块化应用?
- 安装并引入Express
- 执行express方法获取框架核心模块
- 调用模块的use方法配置中间件及路由
- 在router文件中编写路由,通过express.Router()来获取路由对象,使用get或post来配置接收对应请求,再转发至controller
- 在controller中进行业务处理,返回信息
⭐说说Express的中间件
中间件 函数能够访问请求对象 (req
)、响应对象[(res
) 以及应用程序的请求/响应循环中的下一个中间件函数。下一个中间件函数通常由名为 next
的变量来表示。
中间件函数可以执行以下任务:
- 执行任何代码。
- 对请求和响应对象进行更改。
- 结束请求/响应循环。
- 调用堆栈中的下一个中间件函数。
如果在当前中间件执行完毕后,没有结束上面的请求/响应循环,那么中间件必须调用next()函数将该请求的控制权交给下一个中间件。否则该请求将保持挂起状态。
中间件的注册方式:
中间件可以使用app.use()
和 app.METHOD()
函数绑定到应用程序对象的实例。其中METHOD表示的是各种请求方式,如get post等
在注册时可以只传入中间件函数,这样在每次接收到请求后都会执行该中间件
也可以在第一个参数传入路径,这样只有请求到指定路径时才会执行该中间件
可以一次传入多个中间件,这样就会按序生效app.use('/user/:id', middleware, middleware2 ...)
路径可以传动态参数:
javascript
app.use('/user/:id', (req, res, next) => {
console.log(req.params.id);
res.send(req.params.id)
})
中间件的分类:
Express框架当中根据中间件使用方式的不同可以分为以下几类:
-
应用层中间件: 就是直接扔进去一个中间件函数
-
路由器层中间件: 对express.Router()生成的路由配置中间件,再注册给app
-
错误处理中间件: 可传入四个参数,为了正确拦截错误,一般放在中间件链的最后
luaapp.use(function(err, req, res, next) { console.error(err.stack); res.status(500).send('Something broke!'); });
-
内置中间件: Express当中唯一的内置中间件函数是
express.static
,负责提供 Express 应用程序的静态资源phpapp.use(express.static(rootDir, options));
csharp//当访问你的 Express 应用程序时,它将自动提供位于 public 目录下的静态文件👇 //这里__dirname会自动识别你的操作系统,并为其补全绝对路径 app.use(express.static(path.join(__dirname, 'public')));
-
第三方中间件
Koa
⭐koa与express的区别
- koa不提供内置中间件,不提供路由,而是将路由这个库分离了出来
- koa增加了context对象,作为这次请求的上下文对象。context上挂载了request和response对象
- express采用callback来处理异步,koa v1采用生成器,v2采用async/await
- express基于connect中间件,为线性模型,koa中间件则采用洋葱模型。每个中间件在完成处理后,将控制权传递给下一个中间件,并能够等待它完成,当后续的中间件完成处理后,控制权又回到了自己。(通过await next()来完成)
数据库
⭐关系型数据库和非关系型数据库的区别
关系型数据库 最典型的数据结构是表 ,由二维表 及其之间的联系所组成的一个数据组织。其格式一致,支持通用SQL,可以实现复杂查询,支持事务。但由于在硬盘上存储,数据读写性能较差,尤其是在高并发读写时的效率较低,为了支持更高的并发量,其通常纵向扩展,通过提高计算机性能来提高处理能力。
非关系型数据库是一种数据结构化存储方法的集合,可以是文档或者键值对等,它的格式灵活,存储载体不局限于硬盘,存储速度快,部署简单,且非关系型数据库天然是分布式的,所以可以通过集群来实现负载均衡。但缺点是不支持SQL、事务,且复杂查询方面稍欠缺
如何选择?
关系型数据库适合存储结构化数据,比如:用户的账号、地址
非关系型数据库适合存储非结构化数据,比如:文章、评论
如今发展:
随着技术发展,非关系型数据库也并非完全不提供sql支持,不支持事务,现在很多数据库已经实现了。 关系型数据也逐渐可支持复杂数据类型,也并非只能使用硬盘了。比如基于mysql的分布式架构TIDB。
如今单从某些特性定义他是不是关系型数据库已经不准确,也没有意义了,我们只需了解传统意义的关系型数据库是哪几个,有啥显著特点即可
字数达上限,后续内容可以在我博客观看: