前端常见面试题

Xxd Lv2

—HTML/CSS 部分—

1、css 布局方式

table 布局
flex 布局
float 布局
响应式布局

2、盒子模型

标准盒子模型和 IE 盒子模型
主要区别:
IE 盒子模型的宽高包含 content 、 padding 以及 border,标准盒子模型则不包括

box-sizing:content-box 标准盒模型
box-sizing:border-box IE 盒模型

3、html5 新标签

canvas 元素

标签 描述
canvas 标签定义图形,比如图表和其他图像。该标签基于 JavaScript 的绘图 api


新多媒体元素

标签 描述
audio 定义音频内容
video 定义视频(video 或者 movie)
source 定义多媒体资源 video 和 audio
embed 定义嵌入的内容,比如插件等
track 为诸如 video 和 audio 元素之类的媒介规定外部文本轨道


新的语义和结构元素
HTML5 提供了新的元素来创建更好的页面结构

标签 描述
article 定义页面独立的内容区域
aside 定义页面的侧边栏内容
bdi 允许您设置一段文本,使其脱离其父元素的文本方向设置
details 用于描述文档或文档某个部分的细节
dialog 定义对话框,比如提示框
summary 标签包含 details 元素的标题
figure 规定独立的流内容(图像、图表、照片、代码等等)
figcaption 定义 figure 元素的标题
footer 定义 section 或 document 的页脚
header 定义了文档的头部区域
mark 定义带有记号的文本
meter 定义度量衡。仅用于已知最大和最小值的度量
nav 定义导航链接的部分
progress 定义任何类型的任务的进度

4、BFC

BFC(Block Fornmatting Context),即块级格式化上下文,它是页面中一个独立的容器,容器中的元素不会影响到外面的元素

触发条件
触发 BFC 的条件包括但不限于:

根元素,即 HTML 元素
浮动元素:float 值为 left、right
overflow 值不为 visible,为 auto、scroll、hidden
display 值为 inline-block、table-cell、table-caption、table、inline-table、flex、inline-flex、grid、inline-grid
position 值为 absolute 或 fixed

5、浏览器运行机制

1、创建 DOM 树,渲染引擎开始解析 html 文档,将标签转换成 DOM 节点
2、构建渲染树,解析 CSS 样式文件(CSSDOM),并且与 DOM 结合,生成渲染树
3、布局渲染树,从根节点开始递归调用,计算每个元素的大小和位置
4、绘制渲染树,遍历渲染树,每个节点将使用 UI 后端层来绘制

重绘:一个元素外观的改变触发浏览器行为,浏览器会根据元素的新属性重新绘制。当改变元素的外观属性时触发重绘,比如 div 的 color、background-color 等属性发生改变时

回流(重排):当渲染树中,一部分元素的尺寸、布局、隐藏等要改变的时候,回流

重排必定会引起重绘,重绘不一定引起重排

如何优化?

减少 DOM 操作,减少重绘和重排的操作
1、直接修改元素的 className,减少重排次数
2、将需要多次重排的元素的 position 设为 absolute 或 fixed,元素脱离文档流就不会影响其他元素

6、行内元素和块级元素

行内元素
1、和其他同级元素都在同一行
2、高,行高及外边距和内边距部分可以改变
3、宽度只与内容有关
4、行内元素只能容纳文本或者其他行内元素
5、a,img,input,label,select,span,textarea,font

块级元素
1、总是在新行上开始,占据一整行
2、高度,行高以及外边距和内边距全部可控
3、宽度始终与浏览器的宽度一样,与内容无关
4、可以容纳行内元素和块级元素
5、div、p、table、form、h1、h2、h3、dl、ol、ul、li

7、居中方式

· 水平居中
· 垂直居中
· 水平垂直居中

水平居中

1、行内元素

看父级元素是否为块级元素,是则直接给父级元素设置 text-align: center;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<style>
#father {
width: 500px;
height: 300px;
background-color: #fe4a6b;
text-align: center;
}
#son {
font-size: 20px;
}
</style>

<div id="father">
<span id="son">我是行内元素</span>
</div>

如果不是则需要将父元素设置为块级元素,再给父元素设置 text-align: center;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<style>
#father {
display: block;
width: 500px;
height: 300px;
background-color: #fe4a6b;
text-align: center;
}
#son {
font-size: 20px;
}
</style>

<span id="father">
<span id="son">我是行内元素</span>
</span>

效果

2、块级元素

方案一
宽度确定:需要谁居中就给谁设置 margin: 0 auto;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<style>
#father {
width: 500px;
height: 300px;
background-color: #fe4a6b;
}
#son {
width: 100px;
height: 100px;
background-color: #fff;
margin: 0 auto;
}
</style>

<div id="father">
<div id="son">我是块级元素</div>
</div>

效果

宽度不定:默认子元素的宽度和父元素一样,这时需要设置子元素为 display: inline-block;或 display: inline;即将其转换成行内块级/行内元素,给父元素设置 text-align: center;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<style>
#father {
width: 500px;
height: 300px;
background-color: #fe4a6b;
text-align: center;
}
#son {
background-color: #fff;
display: inline;
}
</style>

<div id="father">
<div id="son">我是块级元素</div>
</div>

效果(将#son 转换成行内元素,内容的高度撑起了#son 的高度,设置高度无用)

方案二:使用定位属性(子绝父相)
首先设置父元素为相对定位,再设置子元素为绝对定位,设置子元素的 left: 50%;,即让子元素的左上角水平居中
宽度确定:设置绝对定位子元素的 margin-left: -元素宽度的一半 px; 或设置 transform: translateX(-50%);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<style>
#father {
width: 500px;
height: 300px;
background-color: #fe4a6b;
position: relative;
}
#son {
background-color: #fff;
width: 100px;
height: 100px;
position: absolute;
left: 50%;
margin-left: -50px; /* 负的元素宽度的一半 */
}
</style>

<div id="father">
<div id="son">我是块级元素</div>
</div>

效果

宽度不定:利用 css3 新属性 transform: translate(-50%);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<style>
#father {
width: 500px;
height: 300px;
background-color: #fe4a6b;
position: relative;
}
#son {
background-color: #fff;
height: 100px;
position: absolute;
left: 50%;
transform: translateX(-50%);
}
</style>

<div id="father">
<div id="son">我是块级元素</div>
</div>

效果

方案三:使用 flexbox 布局实现(宽度定不定都行)
使用 flexbox 布局,只需要给待处理的块状元素的父元素添加属性 display: flex; justify-content: center;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<style>
#father {
width: 500px;
height: 300px;
background-color: #fe4a6b;
display: flex;
justify-content: center;
}
#son {
background-color: #fff;
width: 100px;
height: 100px;
}
</style>

<div id="father">
<div id="son">我是块级元素</div>
</div>

效果

垂直居中

1、单行的行内元素

只需要设“行高等于盒子的高”即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<style>
#father {
width: 500px;
height: 300px;
background-color: #fe4a6b;
line-height: 300px;
}
#son {
background-color: #fff;
}
</style>

<div id="father">
<span id="son">我是单行的行内元素</span>
</div>

效果

2、多行的行内元素

使用给父元素设置 display: table-cell;和 vertical-align: middle;属性即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<style>
#father {
width: 500px;
height: 300px;
background-color: #fe4a6b;
display: table-cell;
vertical-align: middle;
}
#son {
background-color: #fff;
}
</style>

<div id="father">
<span id="son">我是多行的行内元素我是多行的行内元素我是多行的行内元素我是多行的行内元素我是多行的行内元素我是多行的行内元素我是多行的行内元素我是多行的行内元素</span>
</div>

效果

3、块级元素

方案一:使用定位
首先设置父元素为相对定位,再设置子元素为绝对定位,设置子元素的 top: 50%;即让子元素的左上角垂直居中
高度确定:设置绝对子元素的 margin-top: -元素高度的一半 px;或者设置 transform:translateY(-50%);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<style>
#father {
width: 500px;
height: 300px;
background-color: #fe4a6b;
position: relative;
}
#son {
background-color: #fff;
height: 100px;
position: absolute;
top: 50%;
margin-top: -50px;
}
</style>

<div id="father">
<div id="son">我是块级元素</div>
</div>

效果

高度不定:利用 css3 新增属性 transform: translateY(-50%);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<style>
#father {
width: 500px;
height: 300px;
background-color: #fe4a6b;
position: relative;
}
#son {
background-color: #fff;
width: 100px;
position: absolute;
top: 50%;
transform: translateY(-50%);
}
</style>

<div id="father">
<div id="son">我是块级元素</div>
</div>

效果

方案二:使用 flexbox 布局实现(高度定不定都可以)
使用 flexbox 布局,只需要给待处理的块状元素的父元素添加属性 display: flex; align-items: center;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<style>
#father {
width: 500px;
height: 300px;
background-color: #fe4a6b;
display: flex;
align-items: center;
}
#son {
background-color: #fff;
width: 100px;
height: 100px;
}
</style>

<div id="father">
<div id="son">我是块级元素</div>
</div>

效果

水平垂直居中

1、已知高度和宽度的元素

方案一
设置父元素为相对定位,给子元素设置绝对定位,top: 0; right: 0; bottom: 0; left: 0; margin: auto;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<style>
#father {
width: 500px;
height: 300px;
background-color: #fe4a6b;
position: relative;
}
#son {
background-color: #fff;
width: 100px;
height: 100px;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
}
</style>

<div id="father">
<div id="son">我是块级元素</div>
</div>

效果

方案二
设置父元素为相对定位,给子元素设置绝对定位,left: 50%; top: 50%; margin-left: -元素宽度的一半 px; margin-top: -元素高度的一半 px;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<style>
#father {
width: 500px;
height: 300px;
background-color: #fe4a6b;
position: relative;
}
#son {
background-color: #fff;
width: 100px;
height: 100px;
position: absolute;
top: 50%;
left: 50%;
margin-left: -50px;
margin-top: -50px;
}
</style>

<div id="father">
<div id="son">我是块级元素</div>
</div>

效果

2、未知高度和宽度的元素

方案一:使用定位属性
设置父元素为相对定位,给子元素设置绝对定位,left: 50%; top: 50%; transform: translateX(-50%); transform: translateY(-50%);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<style>
#father {
width: 500px;
height: 300px;
background-color: #fe4a6b;
position: relative;
}
#son {
background-color: #fff;
position: absolute;
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-50%);
}
</style>

<div id="father">
<div id="son">我是块级元素</div>
</div>

效果

方案二:使用 flex 布局实现
设置父元素为 flex 定位,justify-content: center; align-items: center;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<style>
#father {
width: 500px;
height: 300px;
background-color: #fe4a6b;
display: flex;
justify-content: center;
align-items: center;
}
#son {
background-color: #fff;
}
</style>

<div id="father">
<div id="son">我是块级元素</div>
</div>

效果

8、rem、em、vh、vw、px

px:绝对单位,页面按精确像素展示
em:相对单位,基准点为父节点字体的大小,如果自身定义了 font-size 按自身来计算,整个页面内 1em 不是一个固定的值
rem:相对单位,可理解为 root em, 相对根节点 html 的字体大小来计算
vh、vw:主要用于页面视口大小布局,在页面布局上更加方便简单

vh、vw换算

假设设计稿宽高为 1920px = 100vw 1080px = 100vh
如果一个 div 宽 300px,高 200px,如果转换为 vw 和 vh,换算方式如下
vwDiv = (300px / 1920px) * 100vw 即 15.625vw
vhDiv = (200px / 1080px) * 100vh 即 18.518vh(除不尽取一定位数即可)

9、有哪些方式可以隐藏页面元素?区别?

通过 css 实现隐藏元素方法如下

  • display: none
  • visibility: hidden
  • opacity: 0
  • 设置 height、width 模型属性为 0
  • position: absolute
  • clip-path

关于 display: none、visibility: hidden、opacity: 0 的区别,如下表所示

display: none visibility: hidden opacity: 0
页面中 不存在 存在 存在
重排 不会 不会
重绘 不一定
自身绑定事件 不触发 不触发 可触发
transition 不支持 支持 支持
子元素可复原 不能 不能
被遮挡的元素可触发事件 不能

10、什么是响应式设计?响应式设计的基本原理是什么?如何做?

响应式网站设计(Responsive Web Design)是一种网络页面设计布局,页面的设计与开发应当根据用户行为以及设备环境(系统平台、屏幕尺寸、屏幕定向等)进行相应的响应和调整

响应式网站常见的特点

  • 媒体查询(我们可以设置不同类型的媒体条件,并根据对应的条件,给相应符合条件的媒体调用相对应的样式表
  • 百分比
  • vw/vh
  • rem

响应式设计实现通常会从以下几方面思考

  • 弹性盒子(包括图片、表格、视频)和媒体查询等技术
  • 使用百分比布局创建流式布局的弹性 UI,同时使用媒体查询限制元素的尺寸和内容变更范围
  • 使用相对单位使得内容自适应调节
  • 选择断点,针对不同断点实现不同布局和内容展示

tips

移动端适配将在本章节第16点详细描述

11、css 选择器有哪些以及优先级

关于 css 属性选择器常用的有

  • id 选择器(#box),选择 id 为 box 的元素
  • 类选择器(.one),选择类名为 one 的所有元素
  • 标签选择器(div),选择标签为 div 的所有元素
  • 后代选择器(#box div),选择 id 为 box 元素内部所有的 div 元素
  • 子选择器(.one>one1),选择父元素为.one 的所有.one1 的元素
  • 相邻同胞选择器(.one+.two),选择紧接在.one 之后的所有.two 元素
  • 群组选择器(div,p),选择 div、p 的所有元素


还有一些使用频率相对没那么多的选择器

  • 伪类选择器

    :link 选择未被访问的链接
    :visited 选取已被访问的链接
    :active 选择活动链接
    :hover 鼠标指针浮动在上面的元素
    :focus 选择具有焦点的
    :first-child 父元素的首个子元素

  • 伪元素选择器

    :first-letter 用于选取指定选择器的首字母
    :first-line 选取指定选择器的首行
    :before 选择器在被选元素的内容前面插入内容
    :after 选择器在被选元素的内容后面插入内容

  • 属性选择器

    [attribute] 选择带有 attribute 属性的元素
    [attribute=value] 选择所有使用 attribute=value 的元素
    [attribute~=value] 选择 attribute 属性包含 value 的元素
    [attribute|=value] 选择 attribute 属性以 value 开头的元素


以下为 css3 中新增的选择器

  • 层次选择器(p ~ ul),选择前面有 p 元素的每个 ul 元素
  • 伪类选择器

    :first-of-type 父元素的首个元素
    :last-of-type 父元素的最后一个元素
    :only-of-type 父元素的特定类型的唯一子元素
    :only-child 父元素中唯一子元素
    :nth-child(n) 选择父元素中第 N 个子元素
    :nth-last-of-type(n)选择父元素中第 N 个子元素,从后往前
    :last-child 父元素的最后一个元素
    :root 设置 HTML 文档
    :empty 指定空的元素
    :enabled 选择被禁用元素
    :disabled 选择被禁用元素
    :checked 选择选中的元素
    :not(selector) 选择非 selector 元素的所有元素

  • 属性选择器

    [attribute*=value] 选择 attribute 属性值包含 value 的所有元素
    [attribute^=value] 选择 attribute 属性开头为 value 的所有元素
    [attribute$=value] 选择 attribute 属性结尾为 value 的所有元素

优先级
内联(权值 1000)>ID 选择器(权值 100)>类选择器(权值 10)>标签选择器(权值 1)>通配符选择(权值 0)

12、清除浮动的方法

方法一:额外标签法,给谁清除浮动,就在其后额外添加一个空白标签
优点:通俗易懂,书写方便
缺点:添加许多无意义的标签,结构较差(因此不推荐使用)

1
2
3
4
5
6
7
8
9
10
11
<style>
.clear {
clear: both;
}
</style>

<div class="fahter">
<div class="big">big</div>
<div class="small">small</div>
<div class="clear">额外标签法</div>
</div>

方法二:父级添加 overflow 方法,可以通过触发 BFC 的方式,实现清楚浮动效果。必须定义 width 或 zoom:1,同时不能定义 height,使用 overflow:hidden 时,浏览器会自动检查浮动区域的高度
优点:简单、代码少、浏览器支持好
缺点:内容增多时候容易造成不会自动换行导致内容被隐藏掉,无法显示需要溢出的元素。不能和 position 配合使用,因为超出的尺寸的会被隐藏。

1
2
3
4
5
6
7
<style>
.father {
width: 400px;
border: 1px solid deeppink;
overflow: hidden;
}
</style>

注意

注意: 别加错位置,给父级元素加(并不是所有的浮动都需要清除,谁影响布局,才清除谁。)

方法三、使用 after 伪元素清除浮动,:after 方式为空元素的升级版,好处是不用单独加标签了。IE8 以上和非 IE 浏览器才支持:after,zoom(IE 专有属性)可解决 ie6,ie7 浮动问题(较常用推荐)
优点:符合闭合浮动思想,结构语义化正确,不容易出现怪问题(目前:大型网站都有使用,如:腾迅,网易,新浪等等)
缺点:由于 IE6-7 不支持:after,使用 zoom:1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<style>
.clearfix:after {
/*伪元素是行内元素 正常浏览器清除浮动方法*/
content: "";
display: block;
height: 0;
clear: both;
visibility: hidden;
}
.clearfix {
*zoom: 1; /*ie6清除浮动的方式 *号只有IE6-IE7执行,其他浏览器不执行*/
}
</style>

<div class="father clearfix">
<div class="big">big</div>
<div class="small">small</div>
<!--<div class="clear">额外标签法</div>-->
</div>
<div class="footer"></div>

方法四、使用 before 和 after 双伪元素清除浮动(较常用推荐)
优点:简单、代码少、容易掌握
缺点:只适合高度固定的布局,要给出精确的高度,如果高度和父级 div 不一样时,会产生问题
补充
父级 div 定义 height: 父级 div 手动定义 height,就解决了父级 div 无法自动获取到高度的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<style>
.father {
border: 1px solid black;
*zoom: 1;
}
.clearfix:after,
.clearfix:before {
content: "";
display: block;
clear: both;
}
.big,
.small {
width: 200px;
height: 200px;
float: left;
}
.big {
background-color: red;
}
.small {
background-color: blue;
}
</style>

<div class="father clearfix">
<div class="big">big</div>
<div class="small">small</div>
</div>
<div class="footer"></div>

tips

1、为什么要清除浮动?
清除浮动主要是为了解决,父元素因为子元素浮动引起的内部高度为 0 的问题
当父元素不给高度时,内部元素不浮动的时候就会撑开,而浮动时父元素会变成一条线,所以这个时候就需要解决浮动

2、清除浮动四种方式总结
1、额外标签法(给最后一个浮动的标签后,新加一个标签,给其设置 clear:both;,)(但这种方式是不推荐使用的,因为添加无意义的标签,语义化差)
2、父元素添加 overfiow 属性(触发 BFC 的方式,实现清除浮动)
3、使用 after 伪元素清除浮动
4、使用 before 和 after 双伪元素清除浮动

13、position 的属性

相对定位:相对于当前元素的位置进行移动
绝对定位:如果不把父元素设置为相对定位,则相对于页面的左上角定位
常见定位:子绝父相

语义
static 静态定位
relative 相对定位
absolute 绝对定位
fixed 固定定位

14、如何做好 seo

SEO 即搜索引擎优化,利用搜索引擎规则提高引擎内网站的自然排名,使网站占据更好位置,收获品牌效益。

  • 语义化 html 标签
  • 合理的 title、description、keywords
  • 重要的 html 代码放前面
  • 少用 iframe,搜索引擎不会抓取 iframe 中的内容
  • 图片加上 alt

15、CSS图形实现

1、经典css实现三角形

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div class="triangle">
</div>
<style>
/* 一种是分别写四个边,一种是统一写border样式 */
.triangle{
position: relative;
width: 0px;
height: 0px;
/* border-top: 200px solid black;
border-right: 200px solid transparent;
border-left: 200px solid transparent;
border-bottom: 200px solid transparent; */
border-style: solid solid solid solid;
border-color:red transparent transparent transparent;
border-width:200px 200px 200px 200px;
}
</style>

效果

2、三角形的使用

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<div class="triangleUse">
</div>
<br>
<!-- 斜三角用法 -->
<div class="price">
<span class="miao">
¥1660
<i></i>
</span>
<span class="origin">¥5650</span>
</div>
<style>
.triangleUse{
width: 0;
height: 0;
/* 把上边框宽度调大 */
border-top: 100px solid transparent;
border-right: 50px solid #ff0000;
/* 左边和下边的边框宽度设置为0 */
border-bottom: 0 solid #00aa00;
border-left: 0 solid #ff55ff;
/* 以下为简写 */
/* border-color: transparent red transparent transparent;
border-style: solid;
border-width: 100px 50px 0 0; */
}

.price{
width: 160px;
height: 24px;
line-height: 24px;
border: 1px solid red;
}

.miao{
position: relative;
float: left;
width: 90px;
height: 100%;
background-color: red;
}

.miao i{
position: absolute;
right: 0;
top: 0;
width: 0;
height: 0;
border-width:24px 10px 0 0;
border-color:transparent #fff transparent transparent;
border-style: solid;
}

.origin {
font-size: 12px;
color: gray;
text-decoration: line-through;
}
</style>

效果

3、圆形

代码

1
2
3
4
5
6
7
8
9
10
<div class="circle">
</div>
<style>
.circle{
width: 100px;
height: 100px;
background: #fe4a6b;
border-radius: 50px
}
</style>

效果

4、椭圆形

代码

1
2
3
4
5
6
7
8
9
10
<div class="oval">
</div>
<style>
.oval{
width: 200px;
height: 100px;
background: #fe4a6b;
border-radius: 100px / 50px;
}
</style>

效果

5、多个元素之间通过竖线分割

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<div class="outside1">
<span class="apart1">张三</span>
<span class="apart1">李四</span>
<span class="apart1">王五</span>
</div>
<br>
<div class="outside2">
<span class="apart2">张三</span>
<span class="apart2">李四</span>
<span class="apart2">王五</span>
</div>
<br>
<div class="outside3">
<span>张三</span>
<span class="apart3">|</span>
<span>李四</span>
<span class="apart3">|</span>
<span>王五</span>
</div>
<style>
/* 第一种画法,使用boder,但是很难调到两者水平居中,并且选择器需要排除最后一个元素 */
.apart1:not(:last-child){
border-right: 1px solid black;
}

/* 第二种画法,垂直居中但不水平居中,子绝父相采用伪元素进行画线 */
.outside2{
position: relative;
}

.apart2+.apart2::before{
position: absolute;
content: "";
top: 50%;
transform: translatey(-50%);
width: 1px;
height: 20px;
background-color: #666;

}

/* 第三种,加入“|”进行划线,采用flex布局垂直居中并用space-between均匀分割,实现垂直水平居中 */
.outside3{
width: 200px;
display: flex;
/* flex-flow: row nowrap; */
align-items: center;
justify-content: space-between;
}

.apart3{
font-size: 2vh;
font-weight: 100;
color: #fe4a6b;
}
</style>

效果

6、横线居中

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<div class="other-way">
<span class="txt">横线居中</span>
<span class="line"></span>
</div>
<style>
.other-way{
height: 30px;
line-height: 30px;
text-align: center;
display: flex;
align-items: center;
}

.other-way .txt{
display: inline-block;
width: 120px;
color: white;
background: #fe4a6b;
border-radius: 15px;
margin-right: 20px;
}

.other-way .line{
display: inline-block;
width: 200px;
border: 1px solid black;
}
</style>

效果

16、移动端适配

1.图片适配
img{ max-width: 100%}后图片会自动缩放,同时图片最大显示为其自身的100%,不使用width是因为当容器大于图片宽度时,图片会拉伸

2.媒体查询
语法

1
2
3
4
5
6
7
8
9
10
@media screen and (min-width: 1200px){
body{
background-color: red;
}
}
@media screen and (max-width: 800px){
body{
background-color: blue;
}
}

当屏幕宽度大于1200px时,背景变为红色,小于800px时,背景变为蓝色

3.动态rem
与媒体查询配合,实现响应式布局
媒体查询在宽高规定多少范围的时候,给html、body设置不同的字体大小、宽高等属性。

17、三栏布局的实现

1.浮动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
*{
margin: 0;
padding: 0;
}

.box div{
height: 100px;
}
.left{
width: 300px;
float: left;
background: gray;
}
.right{
width: 300px;
float: right;
background: gray;
}
.center{
background: yellowgreen;
}
</style>
</head>
<body>
<div class="box">
<div class="left">left</div>
<div class="right">right</div>
<div class="center">center</div>
</div>
<!-- 错误方式 -->
<!-- <div class="box">
<div class="left">left</div>
<div class="center">center</div> 一定不要写在中间
<div class="right">right</div>
</div> -->
</body>
</html>

解释以上,left左浮动,right右浮动,center中间自适应宽度,需要注意center不要写到left和right中间,原因在于left脱离文档流了,center百分百占满一行,所以right被挤到下面去了

2.绝对定位
以下减少观感重复只展示css部分不展示全部代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.box{
position: relative;
}
.left{
width: 300px;
position: absolute;
left: 0;
background: gray;
}
.right{
width: 300px;
position: absolute;
right: 0;
background: gray;
}
.center{
position: absolute;
left: 300px;
right: 300px;
background: yellowgreen;
}

以上代码使用子绝父相,左侧300px宽并left设为0居左,同样方式右侧居右,然后中间部分分别距离两边300px(两侧div的宽度)即可

3.弹性布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.box{
display: flex;
}
.left{
width: 300px;
background: gray;
}
.right{
width: 300px;
background: gray;
}
.center{
flex: 1;
background: yellowgreen;
}

18、CSS3动画

1、CSS3中有哪些实现动画的方式?

  • transition:过渡动画,用于在一段时间内渐变地改变元素等属性值
  • animation:关键帧动画,通过定义多个关键帧来实现元素属性的动态变化
  • transform:变换动画,可以改变元素的位置、大小、形状等属性

2、如何实现渐变动画效果?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div class="box"><div>
</template>
<style>
.box{
width: 100px;
height: 100px;
background-color: blue;
transition: background-color 1s ease-in-out;
}

.box:hover{
background-color: red;
}
</style>

未完待续

19、flex:1是做什么的?

flex布局中有一个属性是flex,它的值是数字。flex:1实际代表三个属性的简写,分别是

flex-grow 用来增大盒子,当父盒子的宽度大于子盒子的宽度,父盒子的剩余空间可以利用flex-grow来设置子盒子增大的占比
flex-shrink 用来设置子盒子超过父盒子的宽度后,超出部分进行缩小的取值比例
flex-basis 用来设置盒子的基准宽度,并且basis和width同时存在basis会把width干掉

flex:1;的逻辑就是用flex-basis把width干掉,然后再用flex-grow和flex-shrink增大的增大缩小的缩小,达成最终的效果

flex-grow:1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!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>
.box {
width: 500px;
height: 100px;
background-color: hotpink;
display: flex;
}

.box div {
width: 100px;
}

.box div:nth-child(1) {
flex-grow: 1;
}

.box div:nth-child(2) {
flex-grow: 3;
}
.box div:nth-child(3) {
flex-grow: 1;
}


</style>
</head>
<body>
<div class="box">
<div>1</div>
<div>2</div>
<div>3</div>
</div>
</body>
</html>

子盒子为100+100+100,父盒子500,剩余宽度200
flex将多余等分5,然后各取所需
div1扩大1/5即100+2000.2=140
div2扩大3/5即100+200
0.6=220
div3扩大1/5即100+200*0.2=140

flex-shrink:1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!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>
.box {
width: 500px;
height: 100px;
background-color: hotpink;
display: flex;
}

.box div {
width: 200px;
}

.box div:nth-child(1) {
flex-shrink: 1;
}

.box div:nth-child(2) {
flex-shrink: 2;
}

.box div:nth-child(3) {
flex-shrink: 1;
}
</style>
</head>
<body>
<div class="box">
<div>1</div>
<div>2</div>
<div>3</div>
</div>
</body>
</html>

子盒子200+200+200,父盒子500,剩余宽度100,那么需要进行缩放
flex将多余等分4,然后各取所需
div1是200-1000.25=175
div2是200-100
0.5=150
div3是200-100*0.25=175

20、gird布局了解过吗?

Flex布局是轴线布局,只能指定”项目”针对轴线(主副轴)的位置,可以看作是一维布局。Grid 布局则是将容器划分成“行”和“列”,产生单元格,然后指定”项目所在”的单元格,可以看作是二维布局,Grid布局远比 Flex布局强大。(不过存在兼容性问题,使用之前应看具体需求),像一个个格子一样的排列,更加灵活,更加强大

gird布局有隐藏的网格线,用于帮助定位,所以我们一般也会说gird网格布局。它的布局方式更容易去实现比如淘宝首页侧边栏-推荐商品-大轮播图的类似布局。不过gird虽然很强大,但是学习的知识比较多,学习成本相对较高,兼容性不如flex布局

—JavaScript 部分—

1、ES6 新增了哪些方法

1、includes() 用于判断数组是否包含给定的值 返回一个布尔值
2、find() 用于找出第一个符合条件的数组成员
3、findindex() 返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1
4、set 数据结构 类似于数组,但是成员的值都是唯一的,没有重复的值
5、let 声明变量、const 声明常量(var、let、const 的区别)
6、解构赋值
set 和 map 区别!!!
1、map 是键值对,set 是值的集合,键和值可以是任何的值
2、map 可以通过 get 方式获取值,而 set 不能因为它只有值,set 只能用 has 来判断,返回一个布尔值
3、set 的值是唯一的可以做数组去重,map 由于没有格式限制,可以做数据存储

2、promiseApi

前置概念

回调函数

回调函数的定义非常简单:一个函数被当做一个参数传入另一个函数(内部函数),并且这个函数在外部函数内被调用,用来完成某些任务的函数。就被称为回调函数

回调函数的两种写法

1
2
3
4
const text = ()=>{
document.write('hello world')
}
setTimeout(text,1000)
1
2
3
setTimeout(()=>{
document.write('hello world')
}, 1000)

这里第一个参数就是回调函数,第二个参数是毫秒数,这个函数执行后产生一个子线程,等待1s,然后执行回调函数text,输出hello world

1
2
3
4
5
6
7
setTimeout(()=>{
document.write('hello world')
}, 1000)
console.log('123123')
// 运行结果
// 先输出123123
// 1秒后输出hello world

回调地狱
Promise被用来解决回调地狱的问题,那么首先就必须得知道什么是回调地狱,为什么会产生回调地狱

概念:当一个回调函数嵌套一个回调函数的时候就会出现一个嵌套结构,当嵌套的多了就会出现回调地狱的情况

举例:比如发送三个ajax请求

  • 第一个发送get请求获取数据
  • 第二个请求需要第一个请求结果中的数据作为请求参数
  • 第三个请求需要第二个请求结果中的数据作为请求参数
    代码举例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    $.ajax({
    url: 'http://xxdoge.app/data1',
    type: 'get',
    success(res){
    $.ajax({
    url: 'http://xxdoge.app/data2',
    type: 'post',
    data: {a: res.a, b: res.b},
    success(res1){
    $.ajax({
    url: 'http://xxdoge.app/data3',
    type: 'post',
    data: {a: res1.a, b: res1.b},
    success(res2){
    console.log(res2)
    }
    })
    }
    })
    }
    })
    是不是看到上面这串东西就头疼,一个项目中有大量的数据请求,不妨会有需要一个请求拿到的数据去查找另一个数据的需求。如果项目里全是这样的东西,就陷入了可维护性差的状态。代码体验非常不好,就是屎山写法。为了解决这个问题,就引入了Promise,我们可以使用Promise解决回调地狱的问题

Promise

Promise 构建出来的实例存在以下方法

  • then()是实例状态发生改变时的回调函数,第一个参数是 resolved 状态的回调函数,第二个参数是 rejected 状态的回调函数
  • catch()用于指定发生错误时的回调函数
  • finally()用于指定不管 Promise 对象最后状态如何,都会执行的操作

Promise 构造函数存在以下方法

  • all()用于将多个 Promise 实例,包装成一个新的 Promise 实例
  • race()同样是将多个 Promise 实例,包装成一个新的 Promise 实例
  • allSettled()
  • resolve()
  • reject()
  • try()

Promise 存在三种状态

  • 等待
  • 接受
  • 拒绝

Promise 语法格式

1
2
3
4
5
6
7
8
new Promise(function (resolve, reject) {
// resolve 表示成功的回调
// reject 表示失败的回调
}).then(function (res) {
// 成功的函数
}).catch(function (err) {
// 失败的函数
})

出现new关键字就知道Promise是一个构造函数,用来生成Promise实例,能看出构造函数接受一个函数作为参数的对象(上面函数可以简化成箭头函数),该函数就是Promise构造函数的回调函数,该函数中有两个参数resolve和reject,这两个参数也分别是两个函数

简单的去理解的话resolve函数的目的是将Promise对象状态变成成功状态,在异步操作成功时调用,将异步操作的结果,作为参数传递出去。reject函数的目的是将Promise对象的状态变成失败状态,在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数

代码实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const promise = new Promise((resolve,reject)=>{
//异步代码
setTimeout(()=>{
// resolve(['111','222','333'])
reject('error')
},2000)
})
promise.then((res)=>{
//兑现承诺,这个函数被执行
console.log('success',res);
}).catch((err)=>{
//拒绝承诺,这个函数就会被执行
console.log('fail',err);
})

3、var、let、const 区别

var、let、const 三者区别可以围绕下面五点

  • 变量提升

    var 声明的变量存在变量提升,即变量可以在声明之前调用,值为 undefined
    let 和 const 不存在变量提升,即所声明的变量一定要在声明后使用,否则会把错

  • 暂时性死区

    var 不存在暂时性死区
    let 和 const 存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变量

  • 块级作用域

    var 不存在块级作用域
    let 和 const 存在块级作用域

  • 重复声明

    var 允许重复声明变量
    let 和 const 在用一作用域不允许重复声明变量

  • 修改声明的变量

    var 和 let 可以
    const 声明一个只读的常量。一旦声明,常量的值就不能改变

  • 使用

    能用 const 的情况尽量使用 const,其他情况下大多数使用 let,避免使用 var

4、== 和 === 的区别

相等操作符(==)会做类型转换,再进行值的比较,全等运算符(===)不会做类型转换

let result1 = (“55” === 55); //false,不相等,因为数据类型不同
let result2 = (55 === 55); //true,相等,因为数据类型相同值也相同

null 和 undefined 比较,相等操作符(==)为 true,全等 false

let result1 = (null == undefined); //true
let result2 = (null === undefined); //false

5、数组常用方法

下面前三种是对原数组产生影响的增添方法,第四种则不会对原数组产生影响

  • push() 接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度
  • unshift() 开头添加
  • concat() 首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组,不会影响原始数组

下面三种都会影响原数组,最后一项不影响原数组

  • pop() 删除数组的最后一项,同时减少数组的 length 值,返回被删除的项
  • shift() 删除数组的第一项,同时减少数组的 length 值,返回被删除的值
  • splice() 传入两个参数,分别是开始位置,删除元素的数量,返回包含删除元素的数组
  • slice() 创建一个包含原有数组中一个或多个

即修改原来数组的内容,常用 splice
传入三个参数,分别是开始位置,要删除元素的数量,要插入的任意多个元素,返回删除元素的数组,对原数组产生影响

即查找元素,返回元素坐标或者元素值

  • indexOf() 返回要查找的元素在数组中的位置,如果没找到则返回-1
  • includes() 返回要查找的元素在数组中的位置,找到返回 true,否则 false
  • find() 返回第一个匹配的元素

排序方法

数组有两个方法可以用来对元素重新排序

  • reverse() 将数组元素方向反转
  • sort(首元素地址(必填),尾元素的地址+1(必填),比较函数(非必填))

如果直接 sort(数组名),则从小到大排序(即升序),以下为倒序

1
2
3
4
5
var arr4 = [30, 10, 111, 35, 1899, 50, 45];
arr4.sort(function (a, b) {
return b - a;
});
console.log(arr4); //输出 [1899, 111, 50, 45, 35, 30, 10]

转换方法

常见的转换方法
join()
join()方法接受一个参数,即字符串分隔符,返回包含所有项的字符串

迭代方法

常用来迭代数组的方法(都不改变原数组)如下

  • some() 对数组每一项都运行传入的函数,如果有一项函数返回 true,则这个方法返回 true
  • every() 对数组每一项都运行传入的函数,如果对每一项函数都返回 true,则这个方法返回 true
  • forEach() 对数组每一项都运行传入的函数,没有返回值
  • filter() 对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返回
  • map() 对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组

去重方法

1、利用 ES6 Set 去重(ES6 中最常用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function unique(arr) {
return Array.from(new Set(arr));
}
var arr = [
1,
1,
"true",
"true",
true,
true,
15,
15,
false,
false,
undefined,
undefined,
null,
null,
NaN,
NaN,
"NaN",
0,
0,
"a",
"a",
{},
{},
];
console.log(unique(arr));
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]

2、利用 for 嵌套 for,然后 splice 去重(ES5 中常用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function unique(arr) {
for (var i = 0; i < arr.length; i++) {
for (var j = i + 1; j < arr.length; j++) {
if (arr[i] == arr[j]) {
arr.splice(j, 1);
j--;
}
}
}
return arr;
}
var arr = [
1,
1,
"true",
"true",
true,
true,
15,
15,
false,
false,
undefined,
undefined,
null,
null,
NaN,
NaN,
"NaN",
0,
0,
"a",
"a",
{},
{},
];
console.log(unique(arr));
//[1, "true", 15, false, undefined, NaN, NaN, "NaN", "a", {…}, {…}] //NaN和{}没有去重,两个null直接消失了

3、利用 indexOf 去重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function unique(arr) {
if (!Array.isArray(arr)) {
console.log("type error!");
return;
}
var array = [];
for (var i = 0; i < arr.length; i++) {
if (array.indexOf(arr[i]) === -1) {
array.push(arr[i]);
}
}
return array;
}
var arr = [
1,
1,
"true",
"true",
true,
true,
15,
15,
false,
false,
undefined,
undefined,
null,
null,
NaN,
NaN,
"NaN",
0,
0,
"a",
"a",
{},
{},
];
console.log(unique(arr));
// [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}] //NaN、{}没有去重

4、利用 includes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function unique(arr) {
if (!Array.isArray(arr)) {
console.log("type error!");
return;
}
var array = [];
for (var i = 0; i < arr.length; i++) {
if (!array.includes(arr[i])) {
//includes 检测数组是否有某个值
array.push(arr[i]);
}
}
return array;
}
var arr = [
1,
1,
"true",
"true",
true,
true,
15,
15,
false,
false,
undefined,
undefined,
null,
null,
NaN,
NaN,
"NaN",
0,
0,
"a",
"a",
{},
{},
];
console.log(unique(arr));
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}] //{}没有去重

5、for循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  for (var i = 0; i < arr.length; i++) {
for(var j = i+1;j < arr.length;){
if (arr[j] === arr[i]) {
arr.splice(j,1)
}else{
j++ // 关键代码
}
}
}
var arr = [
1,
1,
"true",
"true",
true,
true,
15,
15,
false,
false,
undefined,
undefined,
null,
null,
NaN,
NaN,
"NaN",
0,
0,
"a",
"a",
{},
{},
];
console.log(unique(arr));
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}] //{}没有去重

6、bind、call、apply 区别

1、bind、call、apply 的作用

关于 bind、call、apply 函数,它们主要用来改变 this 指向,在很多框架中常有用到,而且也是面试官喜欢问到的问题。

call 的用法

1
fn.call(thisArg, arg1, arg2,arg3, ...);

调用 fn.call 时会将 fn 中的 this 指向修改为传入的第一个参数 thisArg,将后面的参数传入给 fn 并立即执行函数 fn。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let obj = {
name: "xiaoming",
age: 24,
sayHello: function (job, hobby) {
console.log(
`我叫${this.name},今年${this.age}岁。我的工作是:${job},我的爱好是:${hobby}。`
);
},
};
obj.sayHello("程序员", "派派"); // 我叫xiaoming,今年24岁。我的工作是: 程序员,我的爱好是: 派派。

let obj1 = {
name: "zhangsan",
age: 30,
};
// obj1.sayHello //Uncaught TypeError: obj1.sayHello is not a function
obj.sayHello.call(obj1, "设计师", "画画"); //输出内容:我叫zhangsan,今年30岁。我的工作是:设计师,我的爱好是:画画。

上面的代码中,obj1 对象也想使用 obj 对象中的 sayHello 方法;我们就可以使用 call 方法调用 obj.sayHello, 并将 obj.sayHello 中的 this 修改为 obj1,把 ‘设计师’, ‘画画’ 这两个参数出给 obj.sayHello。

apply 的用法

1
fn.apply(thisArg, [argsArr]);

fn.apply 的作用和 call 相同:修改 this 指向,并立即执行 fn。区别在于传参形式不同,apply 接受两个参数,第一个参数是要指向的 this 对象,第二个参数是一个数组,数组里面的元素会被展开传入 fn,作为 fn 的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let obj = {
name: "xiaoming",
age: 24,
sayHello: function (job, hobby) {
console.log(
`我叫${this.name},今年${this.age}岁。我的工作是: ${job},我的爱好是: ${hobby}。`
);
},
};
obj.sayHello("程序员", "派派"); // 我叫xiaoming,今年24岁。我的工作是: 程序员,我的爱好是: 派派。

let obj1 = {
name: "zhangsan",
age: 30,
};

obj.sayHello.apply(obj1, ["设计师", "画画"]); // 我叫zhangsan,今年30岁。我的工作是: 设计师,我的爱好是: 画画。

bind 的用法

1
fn.bind(thisArg, arg1, arg2, arg3, ...);

fn.bind 的作用是只修改 this 指向,但不会立即执行 fn,会返回一个修改了 this 指向后的 fn。需要调用才会执行:bind(thisArg, arg1, arg2, arg3, …)()。bind 的传参和 call 相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let obj = {
name: "xiaoming",
age: 24,
sayHello: function (job, hobby) {
console.log(
`我叫${this.name},今年${this.age}岁。我的工作是: ${job},我的爱好是: ${hobby}。`
);
},
};
// obj.sayHello('程序员', '派派'); // 我叫xiaoming,今年24岁。我的工作是: 程序员,我的爱好是: 派派。

let obj1 = {
name: "zhangsan",
age: 30,
};

obj.sayHello.bind(obj1, "设计师", "画画"); // 无输出结果
obj.sayHello.bind(obj1, "设计师", "画画")(); // 我叫zhangsan,今年30岁。我的工作是: 设计师,我的爱好是: 画画。

2、bind、call、apply 的区别

1、相同点

  • 三个都是用于改变 this 指向
  • 接收的第一个参数都是 this 要指向的对象
  • 都可以利用后续参数穿惨

2、不同点

  • call 和 bind 传参相同,多个参数依次传入
  • apply 只有两个参数,第二个参数为数组
  • call 和 apply 都是对函数进行直接调用,而 bind 方法不会立即调用函数,而是返回一个修改 this 后的函数

7、本地存储的方式有哪些?区别以及应用场景?

javascript 本地缓存的方法主要讲述以下四种

  • cookie
  • sessionStorage
  • localStorage
  • indexedDB

区别
关于 cookie、sessionStorage、localStorage 三者的区别主要如下

  • 存储大小:cookie 数据大小不能超过 4K,sessionStorage 和 localStorage 虽然也有存储大小的限制,但比 cookie 大得多,可以达到 5M 或更大
  • 有效时间:localStorage 存储持久数据,浏览器关闭后数据数据不丢失除非主动删除数据;sessionStorage 数据在当前浏览器窗口关闭后自动删除;cookie 设置的 cookie 过期时间之前一直有效,即使窗口或浏览器关闭
  • 数据与服务器之间的交互方式,cookie 的数据会自动的传递到服务器,服务器端也可以写 cookie 到客户端;sessionStorage 和 localStorage 不会自动把数据发给服务器,仅在本地保存

应用场景
了解了上述的前端缓存方式后,针对不同场景的使用选择如下

  • 标记用户与跟踪用户行动的情况,推荐使用 cookie
  • 适合长期保存在本地的数据(令牌),推荐使用 localStorage
  • 敏感账号一次性登录,推荐使用 sessionStorage
  • 存储

8、对闭包的理解以及使用场景

1、什么叫做闭包

闭包就是能够读取其他函数内部变量的函数。由于在 javascript 语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

2、闭包的三大特性

  • 内嵌函数:函数嵌套函数,内嵌函数对函数中的局部变量进行访问
  • 局部变量:在函数内定义有共享意义的局部变量
  • 外部使用:函数向外返回此内嵌函数,外部可通过此内嵌函数访问声明在函数中的局部变量,而此变量在外部是通过其他路径无法访问的
  • 参数和变量不会立即被垃圾回收机制回收

3、闭包的优点和缺点

优点

  • 可读取函数内部的变量
  • 局部变量可以保存在内存中,实现数据共享
  • 执行过程中所有变量都匿名在函数内部

缺点

  • 使函数内部变量存在于内存中,内存消耗大
  • 滥用闭包可能导致内存泄漏
  • 闭包可以在父函数外部改变父函数内部的值,谨慎操作

4、闭包的产生条件

作用域嵌套

父级作用域里生成了一个变量 var i = 0 在子作用域里使用这个变量,这样声明的那个变量 i 就是自由变量,这种作用域嵌套环境叫做闭包环境

5、闭包的使用场景

  • 模拟私有方法
  • setTimeout 循环
  • 匿名自执行函数
  • 结果要缓存场景
  • 实现类和继承

6、使用闭包的注意点

  • 由于闭包会使得函数中的变量被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页性能问题,在 IE 中可能导致内存泄漏。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
  • 闭包会在父函数外部,改变父函数内部变量的值。所以如果把父函数作为对象使用,把闭包当作它的公用方法,把内部变量当作它的私有属性,这是一定要小心不要随便改变父函数内部变量的值。

7、为什么要使用闭包?

  • 使用闭包可以延长局部变量的声明周期,不让局部变量使用后立即释放,被删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 当声明变量 i 在outFn 函数外面时,输出结果为:1,2,3,4
var i = 0;
function outerFn() {
return function innerFn() {
i++;
console.log(i);
};
}
var fn1 = outFn();
fn1();
fn1();

var fn2 = outFn();
fn2();
fn2();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 当声明变量 i 在innerFn函数里面时,输出结果为:1,1,1,1
// (当函数重复调用的时候,其内部的局部变量会被重新声明)
function outFn() {
return function innerFn() {
var i = 0;
i++;
console.log(i);
};
}
var fn1 = outFn();
fn1();
fn1();

var fn2 = outFn();
fn2();
fn2();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*当声明变量 i 在innerFn函数外面,outerFn函数里面时,输出结果为:1,2,1,2
因为声明变量是在outFn函数作用域里,在outFn的子作用域里 i 被使用,所以这时的 i 为自由变量,这时得作用域嵌套环境叫做闭包,形成了闭包。*/
function outFn() {
var i = 0;
return function innerFn() {
i++;
console.log(i);
};
}
var fn1 = outFn(); //i 声明了一次
fn1(); //i 自增一次,变为1
fn1(); //i 自增一次 变为2
var fn2 = outFn(); //i 重新被声明为0
fn2(); //i 自增一次 为1
fn2(); //i 自增一次 为2

9、深拷贝浅拷贝的区别

浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的是内存地址。

在 javascript 中,存在浅拷贝的现象有

  • Object.assign
  • Array.prototype.slice()
  • Array.prototype.concat()
  • 使用拓展运算符实现的复制

深拷贝开辟一个新的栈,两个对象属性完全相同,但是对应两个不同的地址,修改一个对象的属性不会改变另一个对象的属性。

常见的深拷贝方式有

  • JSON.stringify()
  • Object.assign()
  • jquery.extend()
  • 手写递归方法
手写递归
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function deepClone1(obj) {
let objClone = Array.isArray(obj) ? [] : {};
if (obj && typeof obj === "object") {
for (let key in obj) {
if (obj[key] && typeof obj[key] === "object") {
objClone[key] = deepClone1(obj[key]); //循环递归直到属性不为一个object为止
} else {
objClone[key] = obj[key];
}
}
}
return objClone;
}
// 下面为测试部分
let person1 = {
name: "园丁",
job: {
salary: 50000,
address: "高新",
id: {
idCard: 5003888,
},
},
};
let person2 = deepClone2(person1);
console.log(person1);
console.log(person2);
let arr1 = [1, 2, [3, 4], 5];
let arr2 = deepClone1(arr1);
console.log(arr1);
console.log(arr2);
console.log(arr1 === arr2); //false
console.log(person1 === person2); //false
// 结论:说明深拷贝得到的不是同一个对象

10、javascript 数据类型

分为基本数据类型和引用数据类型

基本数据类型

  • Number(数值,包含 NaN)
  • String(字符串)
  • Boolean(布尔值)
  • Undefined(未定义/未初始化)
  • Null(空对象)
  • Symbol(独一无二的值,ES6 新增)
  • Bigint(大整数,能够表示超过 Number 类型大小限制的整数,ES 2020 新增)


引用数据类型

  • Object(对象,Array数组和function函数也属于对象的一种)

11、什么是防抖和节流以及实现方法

防抖(Debounce)节流(Throttle)都是用来控制某个函数在一定时间内触发次数,两者都是为了减少触发频率,以便提高性能或者说避免资源浪费。毕竟 JS 操作 DOM 对象的代价还是十分昂贵的。

应用场景:处理一些频繁触发的事件,例如 mousedown、mousemove、keyup、keydown 等,不然的话,页面很可能会十分卡顿

防抖

防抖就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内再次触发事件,则重新计算函数执行时间

举个例子:
假如一个外卖配送员配送学校外卖(不考虑配送时间)
如果每次只配送一单,那么效率较低
此时假设外卖员接到一个配送订单,他认为可以再等 5 分钟,如果 5 分钟里没有额外的单子就开始配送
如果又接到一个新单子,则再等 5 分钟
直到 5 分钟里没有新的单子再出现,就开始配送

防抖代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 接一个订单和等待时间
function debounce(fn, delay) {
// 定时器null
let timer = null;
return function () {
const context = this;
// 如果接到订单就再等5分钟;
if (timer) {
window.clearTimeout(timer);
}
// 没有接到则直接配送
timer = setTimeout(() => {
fn.apply(context, arguments);
timer = null;
}, delay);
};
}

节流

节流就是指连续触发事件但是在 n 秒里只执行一次函数,节流会稀释函数的执行频率

举个例子:
玩 apex 时,恶灵技能冷却时间为 25s
触发技能后,25s 将不能被触发(冷却时间)

节流代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function throttle(fn, delay) {
// 设置一个触发开关
let allowUse = true;
return function(){
// 如果为true,就触发踏入虚空,否则不能触发
if (allowUse) {
fn.apply(this, arguments);
// 触发后关闭开关
allowUse = false;
// 25s冷却后打开开关
setTimeout(() => {
allowUse = true;
}, delay);
}
}
}

12、如何解决数字精度丢失的问题?

理论上用有限的空间来存储无限的小数是不可能保证精确的,但我们可以处理一下得到我们期望的结果

当你拿到 1.4000000000000001 这样的数据要展示时,建议使用 toPrecision 凑整并 parseFloat 转成数字后再显示,如下

1
parseFloat((1.4000000000000001).toPrecision(12)) === 1.4; // true

封装成方法

1
2
3
function strip(num, precision = 12) {
return +parseFloat(num.toPrecision(precision));
}

也可以使用第三方库,如 Math.js、BigDecimal.js

13、javascript 中内存泄漏的几种情况

1、什么是内存泄漏

内存泄漏是在计算机科学中,由于疏忽或错误造成未能释放已经不再使用的内存

程序的运行需要内存。只要程序提出要求,操作系统或者运行时就必须供给内存

对于持续运行的服务进程,必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃

2、垃圾回收机制

javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存

原理:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存

tips

有了垃圾回收机制,不代表不用关注内存泄露。那些很占空间的值,一旦不再用到,需要检查是否还存在对它们的引用。如果是的话,就必须手动解除引用

3、常见的内存泄漏情况

  • 意外的全局变量
  • 定时器会造成内存泄漏
  • 没有清理对 DOM 元素的引用也会造成内存泄漏
  • 闭包也会造成内存泄漏

14、原型,原型链?有什么特点?

1、了解相关概念

1、什么是函数对象
函数对象就是我们平时所称的函数。构造函数也包含其中,这里提一下构造函数和普通函数的区别
构造函数也是普通函数,和普通函数的主要区别在于调用方式不同,作用也不同(构造函数用来构建实例对象)

普通函数调用方式采用直接调用——‘函数名()’

1
2
3
4
function person() {
console.log(this); // window
}
person();

构造函数的调用方式采用 new 关键字来调用——‘new 函数名()’

1
2
3
4
function Person() {
console.log(this); //Person{}
}
let p = new Person();

2、什么是实例对象
构造函数是用来构建实例对象的,那么被 new 出来的对象或者{}就是实例对象

3、什么是原型对象
原型对象是指所有的函数对象都一定有一个对应的原型对象,构造函数在创建的过程中,系统自动创建出来与构造函数相关联的一个空的对象。

2、产生由来

1、函数对象
在这里我们需要了解的是,Function 函数对象是自动产生的第一个对象,也就是源头—Function 函数对象
除了 Function 函数对象之外的所有函数对象,都是由 Function 函数对象创建的,除了我们用户自己写的函数,Function 函数自动就会创建很多函数对象,例如 Object,Window,Date 等,其中 Object 是 Function 自动创建的第一个函数对象。

2、原型对象
所有的函数对象都一定有一个原型对象与之对应,所有的原型对象都是由 Object 函数对象创建的。

3、实例对象
实例化对象都是被对应的函数对象创建的

1
2
3
4
5
6
function Person(id, name) {
this.id = id;
this.name = name;
}
var p1 = new Person(1, "张三");
var p2 = new Person(2, "李四");

实参传给形参,变量和对应的值是保存在实例对象里面,不是函数对象

4、函数对象和原型对象之间的关系
(1)prototype
所有的函数对象中都有一个名字叫做 prototype 的引用类型变量,该引用类型变量是函数对象的成员,值是对应的原型对象的引用值,即 prototype 指向原型对象

prototype 旁边的空间当作引用类型变量 prototype 开辟的内存空间
101、111、1111 为保存的对应原型对象的引用值,通俗来说就是地址!指向对应的原型对象

(2)constructor
一般称之为构造函数,它用于返回创建对象的函数,也就是说它的指向是对应的函数对象。(上图中函数对象指向原型对象的箭头全部反过来)
(3)_proto_
对象想要查看原型那么就需要通过隐式属性_proto_
这里需要注意_proto_的指向问题,注意红色箭头,指向谁就是创建谁


蓝色箭头,表示 proto 指向

Function 函数对象中_proto_指向 Function 原型对象

Object 原型对象的_proto_值为 null

除 Function 函数对象和 Object 原型对象之外。对象中的_proto_指向,看创建了_proto_所属的对象,就指向的原型对象。

  • 比如说 p1 和 p2 是由 Person 函数对象创建的,那么它们的_proto_都指向创建它们的(Person 函数对象)的原型对象(Person 原型对象)
  • 比如说 Object 和我们自己造的 Person 函数对象都是由 Function 函数对象创建的,那么这两个东西的_proto_指向就是 Function 原型对象
  • 再举个例子,Function 原型对象和 Person 原型对象,这俩玩意儿都是 Object 函数对象创建的,那么这俩的_proto_很明显,就是指向 Object 原型对象

对象访问机制
在 js 代码中会看到 p1.show()这样的代码,这表示访问实例对象 p1 中的 show,如果 p1 实例对象里有 show 则找到,如果 p1 里没有 show 则遵循以下对象访问规则

对象访问成员的过程:

  • 当前对象中如果有该成员就找到该成员,访问结束
  • 如果没有该成员,则到_proto_指向的对象中找成员,找到就结束
  • 如果还是没找到,再通过_proto_指向的对象继续找

上述描述看起来比较绕,举一个简单的例子

注意现在 show 在 Object 原型对象中,p1 实例里没有 show,那么用上面的这个过程来访问

p1.show()现在自己的 p1 实例对象里找 show,如果有就访问结束,但是发现并没有
然后就到 p1 的_proto_指向的对象(创建自己的函数对象的原型对象,也就是Person 原型对象)里找,发现也没有
于是再通过 Person 原型对象的_proto_指向的 Object 原型对象里去找,找到了 show

那如果在 Object 里也没找到呢,就报错了。因为 Object 的_proto_值为 null,无法再继续找下去了
所以是不是发现了一种现象,不管怎么找,如果在这一过程中没有找到该成员,那么这一步总会找到 Object 的头上

代码举例
1
2
3
4
5
6
7
8
9
10
11
12
13
// Object已经是原型链的尽头了,这里如果还没找到description那么就只能显示undefined了
Object.prototype.description = "如果你在构造函数里也找不到description,那可以找我,我是原型链的尽头"
function Fn(){
this.name = name
}

//注释这里,Fn的__proto__就去找Fn的原型对象也就是Object.prototype,输出Object.prototype.description的内容
Fn.prototype.description = "如果new出来的对象没有description,那可以通过原型链找到我"

let f1 = new Fn()
//注释这里,f1的__proto__就去找f1的原型对象也就是Fn.prototype,输出Fn.prototype.description的内容
f1.description = "我是description,我是通过f1对象绑定的"
console.log(f1.__proto__)

那么可以得到一个小结论
Object 的原型对象中的成员,可以让所有对象访问到,Object 是原型链的尽头。Object 原型对象通过 Object.prototype 得到。

示例

Object.prototype.show() {…} ,所有对象都可以访问到 show 函数

3、原型链

1、原型链总结:

上面已经简单描述了原型链的大致样子了,那么总结一下:

  • 所有的函数对象中都有一个叫做 prototype 的引用类型变量,它是函数对象的成员,它的值是对应的原型对象的引用值,即prototype 指向原型对象
  • 所有的原型对象中都有一个叫做 constructor 的引用类型变量,它是原型对象的成员,它的值是对应的函数对象的引用值,即constructor 指向函数对象
  • 所有对象中都有一个叫做_proto_的引用类型变量,它是对象的成员,对象中的_proto_的值是哪个对象的引用值?即指向哪个对象?分三种情况:
    (1) Function 函数对象中的_proto_指向 Function 原型对象
    (2) Object 原型对象中_proto_的值为 null
    (3) 除去 Function 函数对象和 Object 原型对象。其他对象就看谁创建了_proto_所属的对象,就指向谁的原型对象
2、原型链概述

当在实例化的对象中访问一个属性时,首先会在该对象内部(自身属性)寻找,如找不到,则会向其_proto_指向的原型中寻找,如仍找不到,则继续向原型中_proto_指向的上级原型中寻找,直至找到或 Object.prototype._proto_为止(值为 null),这种链状过程即为原型链

通过原型链可以实现 JS 的继承,把父类的原型对象赋值给子类的原型,这样子类实例就可以访问父类原型上的方法了。

3、原型链推论
  • 一个对象中如果有 prototype,则该对象一定是函数对象,如果对象为函数对象,则其中一定有 prototype
  • 所有的函数对象中都有 prototype 属性,prototype 总是指向对应的原型对象,原型对象和 new 出来的对象中没有
  • 一个对象中如果有 constructor,则该对象一定是原型对象,如果对象为原型对象,则其中一定有 constructor
  • 所有的原型对象都是由 Object 函数对象创建的。
  • new 出的对象(实例对象)是由函数对象创建的。
  • 所有的对象中都自带属性proto,proto指向一个对象(谁创建了它就指向谁的原型。)
  • Object 原型对象中_proto_特殊,它的值为 null
  • Function 函数对象中_proto_特殊,它指向 Function 原型对象


tips

① _proto_ 和 constructor 属性是每个对象都具有的;
② prototype 属性是函数对象所独有的。
由于 JavaScript 中一切皆对象,即函数对象也是一种对象,所以函数也拥有_proto_和 constructor 属性。

4、重要!!!原型链的作用
  • 继承,用来实现基于原型的继承与属性的共享

  • 避免了代码冗余,减少了内存占用,公用的属性和方法,可以放到原型对象中,这样,通过该构造函数实例化的所有对象都可以使用该对象的构造函数中的属性和方法!


    tips

    可以尝试一下把封装的方法放到 Object.prototype 上,然后就会发现所有的对象都可以访问到了。但是要注意,访问成员的过程里不要出现名命名同的成员。

15、如何实现上拉加载,下拉刷新

一般来说这一功能是在移动端出现的较多,变相的分页功能
第三方库,我本人使用过的是 jquery 的 dropload 插件,其他的还有 pulltorefresh、better-scroll 等

虽然项目工程中大都是直接使用第三方库实现,这里还是提供原生写法的思路,了解原生方式有助对第三方别人写好的东西有更好的理解和使用:

1、上拉加载
本质就是页面触底,或者快要到底时触发事件
触底公示:scrollTop + clientHeight >= offsetTop

1
2
3
4
5
6
7
8
9
let clientHeight = document.documentElement.clientHeight; //浏览器高度
let scrollHeight = document.body.scrollHeight;
let scrollTop = document.documentElement.scrollTop;

let distance = 50; //距离视窗还有50的时候,开始触发;

if (scrollTop + clientHeight >= scrollHeight - distance) {
console.log("开始加载数据");
}

2、下拉刷新
本质是页面本身置于顶部时,用户下拉时需要触发的动作

1
2
3
4
5
6
7
8
9
10
11
<main>
<p class="refreshText"></p>
<ul id="refreshContainer">
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
<li>555</li>
...
</ul>
</main>
  • 监听原生 touchstart 事件,记录其初始位置的值,e.touches[0].pageY
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var _element = document.getElementById("refreshContainer"),
_refreshText = document.querySelector(".refreshText"),
_startPos = 0, // 初始的值
_transitionHeight = 0; // 移动的距离

_element.addEventListener(
"touchstart",
function (e) {
_startPos = e.touches[0].pageY; // 记录初始位置
_element.style.position = "relative";
_element.style.transition = "transform 0s";
},
false
);
  • 监听原生 touchmove 事件,记录并计算当前滑动的位置值与初始位置值的差值,大于 0 表示向下拉动,并借助 CSS3 的 translateY 属性使元素跟随手势向下滑动对应的差值,同时也应设置一个允许滑动的最大值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
_element.addEventListener(
"touchmove",
function (e) {
// e.touches[0].pageY 当前位置
_transitionHeight = e.touches[0].pageY - _startPos; // 记录差值

if (_transitionHeight > 0 && _transitionHeight < 60) {
_refreshText.innerText = "下拉刷新";
_element.style.transform = "translateY(" + _transitionHeight + "px)";

if (_transitionHeight > 55) {
_refreshText.innerText = "释放更新";
}
}
},
false
);
  • 监听原生 touchend 事件,若此时元素滑动达到最大值,则触发 callback,同时将 translateY 重设为 0,元素回到初始位置
1
2
3
4
5
6
7
8
9
10
_element.addEventListener(
"touchend",
function (e) {
_element.style.transition = "transform 0.5s ease 1s";
_element.style.transform = "translateY(0px)";
_refreshText.innerText = "更新中...";
// todo...
},
false
);

16、作用域链的理解

1、作用域

作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合

换句话说,作用域决定了代码区块中变量和其他资源的可见性

举例:

1
2
3
4
5
function myFunction() {
let variable = "内部变量";
}
myFunction(); //执行函数,否则不知道里面是啥
console.log(variable); // 报错,提示variable没有被定义

上述代码,在函数 myFunction 内部创建了一个变量 variable,当我们全局访问这个变量的时候,系统会报错。这也就说明了我们在全局是无法获取到(除闭包外)函数内部的变量。

一般将作用域分为:

  • 全局作用域
  • 函数作用域
  • 块级作用域(es6)

全局作用域
任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序任何位置访问

全局作用域
1
2
3
4
5
6
var greeting = "Hello World!"; // 全局变量
function greet() {
console.log(greeting);
}
greet(); // 打印 'Hello World!'
console.log(greeting); // 打印 'Hello World!'

函数作用域
函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外访问

函数作用域
1
2
3
4
5
6
function greet() {
var greeting = "Hello World!"; // 内部声明变量
console.log(greeting);
}
greet(); // 打印 'Hello World!'
console.log(greeting); // 报错,没有定义

可见上述代码中在函数内部声明的变量或函数,在函数外部是无法访问的,这说明在函数内部定义的变量或者方法只是函数作用域

块级作用域
ES6 引入了 let 和 const 关键字,和 var 关键字不同,在大括号中使用 let 和 const 声明的变量存在于块级作用域中。在大括号之外不能访问这些变量

块级作用域
1
2
3
4
5
6
7
{
let greeting = "Hello World!"; // 块级作用域中的变量
var lang = "English";
console.log(greeting); // Prints 'Hello World!'
}
console.log(lang); // 变量 'English'
console.log(greeting); // 报错:Uncaught ReferenceError: greeting is not defined

2、词法作用域(静态作用域)

变量被创建时就确定好了,而不是执行阶段确定的,就是说我们写代码时它的作用域已经确定了

静态作用域
1
2
3
4
5
6
7
8
9
var a = 2;
function foo() {
console.log(a);
}
function bar() {
var a = 3;
foo(); // 2
}
bar();

JavaScript 遵循词法作用域(静态作用域),相同层级的 foo 和 bar 就没有办法访问到彼此块作用域中的变量,所以输出 2

3、作用域链

当在 Javascript 中使用一个变量的时候,首先 Javascript 引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域

如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错

举例

1
2
3
4
5
6
7
8
9
10
11
12
var sex = "男";
function person() {
var name = "张三";
function student() {
var age = 18;
console.log(name); // 张三
console.log(sex); // 男
}
student();
console.log(age); // Uncaught ReferenceError: age is not defined
}
person();

上述代码做了以下工作

  • student 函数内部属于最内层作用域,找不到 name,于是往上一层 person 函数内部找,找到了 name,输出‘张三’
  • student 函数内部找不到 sex,于是往上一层 person 函数里找,仍找不到继续往外找,到了全局作用域,找到了 sex,输出‘男’
  • 在 person 作用域里输出 age,由于 person 这一层里没有 age,于是往外找,到了全局作用域,仍然找不到 age,于是报错

17、typeof 和 instanceof 的区别

都是判断数据类型的方法,先来看一下各自的使用方法再来讲区别

typeof

typeof运算符返回一个字符串,表示操作数的类型
使用方法

1
2
typeof operand;
typeof operand;

operand表示要返回类型的对象或基本类型的表达式

1
2
3
4
5
6
7
8
typeof 666; // 'number'
typeof "666"; // 'string'
typeof undefined; // 'undefined'
typeof true; // 'boolean'
typeof Symbol(); // 'symbol'
typeof null; // 'object'
typeof []; // 'object'
typeof {}; // 'object'

从上面例子可以看出,typeof 可以精准的判断基本数据类型(null 除外)

instanceof

instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链
使用方法

1
object instanceof constructor;

object是指某个实例对象
constructor是指某个构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 定义构造函数
function C() {}
function D() {}

var o = new C();

o instanceof C; // true,因为 Object.getPrototypeOf(o) === C.prototype
o instanceof D; // false,因为 D.prototype 不在 o 的原型链上

o instanceof Object; // true,因为 Object.prototype.isPrototypeOf(o) 返回 true
C.prototype instanceof Object; // true,同上

C.prototype = {};
var o2 = new C();
o2 instanceof C; // true
o instanceof C; // false,C.prototype 指向了一个空对象,这个空对象不在 o 的原型链上。

D.prototype = new C(); // 继承
var o3 = new D();
o3 instanceof D; // true
o3 instanceof C; // true 因为 C.prototype 现在在 o3 的原型链上

instanceof的实现原理,可以参考下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @description 判断对象是否属于某个构造函数
* @prams left: 实例对象 right: 构造函数
* @return boolean
*/
function myInstanceof(left, right) {
let rightPrototype = right.prototype; // 获取构造函数的显式原型
let leftProto = left.__proto__; // 获取实例对象的隐式原型
while (true) {
// 说明到原型链顶端,还未找到,则返回 false
if (leftProto === null) {
return false;
}
// 隐式原型与显式原型相等
if (leftProto === rightPrototype) {
return true;
}
// 获取隐式原型的隐式原型,重新赋值给 leftProto
leftProto = leftProto.__proto__;
}
}

可以简单理解为:顺着原型链去找,直到找到相同的原型对象,返回true,否则为false

区别

  • typeof会返回一个运算数的基本类型,instanceof返回的是布尔值
  • instanceof可以准确判断引用数据类型,但是不能正确判断基本数据类型
  • typeof虽然可以判断基本数据类型(null 除外),但是无法判断引用数据类型(function 除外)

拓展

可以发现这两者用来检测数据类型都有缺陷,那么可以使用Object.prototype.toString.call()方法解决问题

1
2
3
4
5
6
Object.prototype.toString.call({}); // Object
Object.prototype.toString.call([]); // Array
Object.prototype.toString.call(666); // Number
Object.prototype.toString.call(true); // Boolean
Object.prototype.toString.call(null); // Null
Object.prototype.toString.call(undefined); // Undefined

18、ajax、axios、jsonp 的理解

1、jsonp 是一种可以解决跨域问题的方式,就是通过动态创建 script 标签用 src 引入外部文件实现跨域,script 加载实际上就是一个 get 请求,并不能实现 post 请求。(其他实现跨域的方式:iframe,window.name,postMessage,CORS…)

2、ajax 是一种获取数据的技术,包含了 get 和 post 请求,但仅仅是获取数据,不具备实现跨域的能力,只有后台服务器配置好 Access-Control-Allow-Origin 才可以实现跨域请求

3、如果使用的是 jquery 封装好的 ajax,也就是$.ajax。jquery同时也封装好了jsonp,因此在使用$.ajax 时只要配置好 dataType 为 jsonp 就能够实现 get 请求的跨域,但仍不能实现 post 请求,post 依旧需要由后台服务器配置好跨域,以及自身配置好 json

4、axios 是通过 promise 实现对 ajax 技术的一种封装

名称 原理 用途
ajax 客户端发送请求,请求交给 xhr,xhr 把请求提交给服务器,服务器进行业务处理,服务器响应数据交给 xhr 对象,xhr 对象接收数据,由 javascript 把数据写到页面上 发起网络请求
axios 底层封装是 XMLHttpRequest 对象,实现原理跟 Ajax 同样 是一个基于 promise 的专门用于网络请求的库
jsonp 由于浏览器同源策略的限制,网页中无法通过 Ajax 请求非同源的接口数据。但是 script 标签不受浏览器同源策略的影响,可以通过 src 属性,请求非同源的 js 脚本 src=”xxx”不受同源策略限制解决跨域请求 解决浏览器跨域问题

19、ajax 请求过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ajax 提交 post 请求的数据
// 1. 创建核心对象
var xhr = new XMLHttpRequest();
// 2. 准备建立连接
xhr.open("POST", "register.php", true);
// 3. 发送请求
// 如果要POST提交数据,则需要设置请求头
// 有的面试官会问为什么要设置请求头? 知道请求正文是以什么格式
// Content-Type: application/x-www-form-urlencoded,请求正文是类似 get 请求 url 的请求参数
// Content-Type: application/json,请求正文是一个 json 格式的字符串
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
// 发送数据
xhr.send(querystring);
// 4. 处理响应
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
// 请求处理完毕,响应就绪
if (xhr.status === 200) {
// 请求成功
var data = xhr.responseText;
console.log(data);
}
}
};

20、ajax 请求的时候 get 和 post 方式的区别

1、get 请求不安全,post 安全

2、get 请求数据有大小限制,post 无限制

3、get 请求参数会在 url 中显示,容易被他人窃取,post 在请求体中,不会被窃取

4、post 需要设置请求头

21、什么是事件委托以及优缺点

js 事件委托的原理就是利用冒泡机制,把本应该添加到某个元素上的事件委托给他的父级,从而减少 DOM 交互达到网页优化。

优点:

  • 可以大量节省内存占用,减少事件注册。比如 ul 上代理所有 li 的 click 事件就很不错。
  • 可以实现当新增子对象时,无需再对其进行事件绑定,对于动态内容部分尤为合适

缺点:

  • 事件代理的常用应用应该仅限于上述需求,如果把所有事件都用事件代理,可能会出现事件误判。即本不该被触发的事件被绑定上了事件。
一个简单的事件委托例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<ul id="ul1">
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
<li>555</li>
</ul>;

<script>
var oUl1 = document.getElementById("ul1");
myAddEvent(oUl1, "click", function (e) {
var e = e || window.event;
var target = e.target || e.srcElement;
if (target.nodeName === "LI") {
alert(target.innerHTML);
target.style.background = "red";
}
});
// 事件绑定封装成js函数
function myAddEvent(obj, ev, fn) {
if (obj.attachEvent) {
// ie
obj.attachEvent("on" + ev, fn);
} else {
obj.addEventListener(ev, fn, false);
}
}
</script>

22、事件循环(EventLoop)

js是单线程的,那么面对多任务时,这些任务的执行顺序是什么呢?

同步任务

首先,用一个栈来表示主线程

当多个同步任务时,这些同步任务会依次入栈出栈

如上,同步任务1先入栈,执行完后出栈,然后同步任务2入栈,依此类推

异步任务

异步任务会在同步任务执行之后再执行,那么如果异步任务代码在同步任务之前呢?在js机制中存在一个队列,叫做任务队列,专门用来存放异步任务。也就是说,当异步任务实现时,会先将异步任务存入任务队列里,当执行完所有同步任务后,再去调用任务队列中的异步任务

举例,现在有两个同步任务,两个异步任务

js会先将同步任务1提至主线程,然后发现异步任务1和2,则将异步任务1和2依次放入任务队列

然后同步任务1执行完之后出栈,同步任务2入栈执行

当同步任务2执行完,即所有的同步任务都已完成,再从任务队列中提取异步任务执行

异步任务分类
js中,异步任务分为宏任务和微任务,所以,上述任务队列也分为宏任务队列和微任务队列,那么,什么是宏任务,什么是微任务?

I/O、定时器、事件绑定、ajax等都是宏任务

Promise的then、catch、finally和process的nextTick都是微任务

注意:Promise的then等方法是微任务,而Promise里的代码是同步任务,并且process的nextTick执行顺序优于Promise等方法,因为process.nextTick是直接告诉浏览器说要尽快执行,而不是放入队列

js中,微任务总是先于宏任务执行,也就是说,这三种任务的执行顺序是:同步任务>微任务>宏任务

来道例题考验考验你

1
2
3
4
5
6
7
8
9
10
11
12
console.log(1);
setTimeout(function(){
console.log(2)
},0)
new Promise((resolve,reject)=>{
console.log(3)
resolve()
console.log(4)
}).then(()=>{
console.log(5)
})
console.log(6)

上面执行结果是 1 3 4 6 5 2

解释一下为什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
console.log(1);           // 同步任务,直接输出,目前是1
setTimeout(function(){
console.log(2) // 异步任务中的宏任务,进入任务队列
},0)
new Promise((resolve,reject)=>{ // Promise里的是同步任务
console.log(3) // 同步任务,直接输出,目前是1、3
resolve() // resovle执行的是then代码,then是异步微任务,所以进入任务队列 // 同步任务,直接输出,目前是1、3、4
console.log(4)
}).then(()=>{
console.log(5) //微任务,等待
})
console.log(6) // 同步任务,直接输出,目前是1、3、4、6

// 所有同步任务执行完毕,目前输出是1、3、4、6
// 开始执行异步任务,当前任务队列里是定时器和then
// 定时器是宏任务,promise的then是微任务
// 先执行then,输出1、3、4、6、5
// 后执行定时器,输出1、3、4、6、5、2

23、跨域和同源策略

同源策略是浏览器的一种机制,只允许同源,也就是同协议、同域名、同端口的情况下才能进行数据交互。但是在开发过程中,往往一个项目的接口不止一个域,所以往往需要做跨域处理,通常的跨域策略有以下几种:

  • 1、jsonp,依赖script标签不受同源策略影响,src指向某一个接口的地址,同步需要传递callback回调函数名字,这样当接口调用成功后,本地创建的全局回调函数就会执行,并且接收到数据。不使用img标签的原因是img标签无法执行js语句

  • 2、CORS,依赖服务端对前端的请求头信息进行放行,不做限制

    Access-Control-Allow-origin配置成*

  • 3、代理访问,前端访问不存在跨域问题的代理服务器,代理服务器去访问目标服务器(服务器之间没有跨域限制)
    举例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // vue.config.js文件
    // 在defineConfig中设置
    devServer: {
    proxy: {
    "/apiurl": {
    target: "http://localhost:3000", //这是node默认启动端口号
    changeOrigin: true
    }
    }
    }

    以上代码表示所有通过/apiurl的请求都会代理到http://localhost:3000去,就能够正常访问接口了

24、线程和进程的区别

进程是资源分配的最小单元,线程是代码执行的最小单元

一个应用程序可能会开启多个线程,进程之间数据不共享,一个进程内部可以开启多个线程,线程之间数据可以共享,所以多线程的情况下,往往要考虑线程间的执行顺序问题

浏览器其实能通过webWorkers开启多线程

25、map、forEach、for of、for in循环的区别

  • map无法通过break中断循环,它一定会遍历到结束,然后返回数据,map的遍历速度最快,常用于进行数据结构的转换
  • forEach同样无法中断循环,会遍历完整个数组,相比之下for循环可以中断。(forEach可以通过return达到continue的效果)
  • for of只能遍历数组不能遍历对象,可以遍历每一项的属性值并且可以通过return跳出循环
  • for in既可以遍历数组也可以遍历对象,它可以遍历出每一项的key,不能直接遍历它具体的属性值

—Vue 部分—

写在开头

请注意,由于markdown代码块高亮支持的语言语法不支持框架语法,因此以下代码块语言标注为html的都是vue代码。

1、为什么使用虚拟 DOM(常问)

  • 创建真实 DOM 的代价高:真实的 DOM 节点 node 实现的属性很多,而 vnode 仅仅实现一些必要的属性,想比起来创建一个 vnode 的成本比较低
  • 触发多次浏览器重绘以及回流:使用 vnode,相当于加了一个缓冲,让一次数据变动带来的所有 node 变化,现在 vnode 中进行修改,然后 diff 之后对所有产生差异的节点集中一次对 DOM 树进行修改,以减少浏览器的重绘以及回流
  • 虚拟 DOM 由于本质是一个 js 对象,因此天生具备跨平台能力,可以实现在不同平台的准确显示
  • Virtual DOM 在性能上的收益并不是最主要的,更重要的是它使得 Vue 具备了现代框架应有的高级特性

diff算法

前置知识:虚拟DOM是什么?
答:虚拟DOM是表示真实DOM的js对象

以下为一段真实DOM以及对应的虚拟DOM

真实DOM
1
2
3
4
<div class="container">
<p class="item">xxdoge</p>
<strong class="item">你好呀</strong>
</div>
虚拟DOM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let Vnode = {
tagName: 'div',
props: {
'class': 'container',
},
children: [
{
tagName: 'p',
props: {
'class': 'item',
},
text: 'xxdoge'
},
{
tagName: 'strong',
props: {
'class': 'item',
},
text: '你好呀'
}
]
}

主要问题:什么是Diff算法
如果将刚才的html标签中的strong标签里的文本内容进行修改,那么虚拟dom中children下的tagName为strong的text变为了修改后的内容,而结合diff算法比对找出差异后就能最小化更新视图
答:本质上就是比较两个js对象的差异
大致流程

举个例子,比如说我们有一个ul列表,内部有三个li。当我们需要改变其中某个li或者添加删除一个li的时候,diff算法会以最小的代价帮我们更新视图,这个最小的代价就是通过比对算法直接去找不同点来给ul里修改/增加/删除li,而不是像以往操作真实dom一样重新渲染一个完整的ul。有了diff算法就能在不过多浪费性能的情况下更改我们想要更改的视图,另外需要知道的是diff算法是在同层的vnode节点进行对比以更快速得找到新旧虚拟dom节点的不同之处。

具体如何同层比较新旧节点以及使用到的比对方式“首位指针法”移步b站up主思学堂讲解视频: https://www.bilibili.com/video/BV1JR4y1R7Ln/?spm_id_from=333.337.search-card.all.click&vd_source=86e7162405bc6714fe7baca04c5faa27

2、Vue 组件通信

1、props/$emit

父组件通过 props 向子组件传递数据,子组件通过$emit 和父组件通信
(1)父组件向子组件传值(props 的用法)
props 的特点:

  • props 只能是父组件向子组件进行传值,props 使得父子组件之间形成一个单向的下行绑定。子组件的数据会随着父组件的更新而响应式更新
  • props 可以显示定义一个或多个的数据,对于接受的数据,可以是各种数据类型,同样也可以是传递一个函数
  • props 属性名规则:若在 props 中使用驼峰命名,模版中标签需要使用短横线的形式来书写

用法
父组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 父组件
<template>
<div id="father">
<son :msg="msgData" :fn="myFunction"></son>
</div>
</template>

<script>
import son from "./son.vue";
export default {
name: father,
data() {
msgData: "父组件数据";
},
methods: {
myFunction() {
console.log("vue");
},
},
components: {
son,
},
};
</script>

子组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 子组件
<template>
<div id="son">
<p>{{ msg }}</p>
<button @click="fn">按钮</button>
</div>
</template>
<script>
export default {
name: "son",
props: ["msg", "fn"],
};
</script>

(2)子组件向父组件传递数据($emit 的用法)
$emit 的特点:

  • $emit 绑定一个自定义事件,当这个事件被执行的时候就会将参数传递给父组件,而父组件通过 v-on 监听并接收参数

用法
父组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 父组件
<template>
<div class="section">
<com-article
:articles="articleList"
@onEmitIndex="onEmitIndex"
></com-article>
<p>{{ currentIndex }}</p>
</div>
</template>

<script>
import comArticle from "./test/article.vue";
export default {
name: "comArticle",
components: { comArticle },
data() {
return {
currentIndex: -1,
articleList: ["红楼梦", "西游记", "三国演义"],
};
},
methods: {
onEmitIndex(idx) {
this.currentIndex = idx;
},
},
};
</script>

子组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//子组件
<template>
<div>
<div
v-for="(item, index) in articles"
:key="index"
@click="emitIndex(index)"
>
{{ item }}
</div>
</div>
</template>

<script>
export default {
props: ["articles"],
methods: {
emitIndex(index) {
this.$emit("onEmitIndex", index); // 触发父组件的方法,并传递参数index
},
},
};
</script>

2、ref/$refs

这种方式也是实现父子之间的通信
ref:这个属性用在子组件上,它的作用就是指向子组件的实例,可以通过实例在访问组件的数据和方法
用法
子组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
export default {
data() {
return {
name: "JavaScript",
};
},
methods: {
sayHello() {
console.log("hello");
},
},
};
</script>

父组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<child ref="child"></component-a>
</template>
<script>
import child from './child.vue'
export default {
components: { child },
mounted () {
console.log(this.$refs.child.name); // JavaScript
this.$refs.child.sayHello(); // hello
}
}
</script>

3、eventBus 事件总线($emit/$son)

eventBus 事件总线适用于父子组件、非父子组件等之间的通信,使用步骤如下
(1)创建事件中心管理组件之间的通信

1
2
3
// event-bus.js
import vue from "vue";
export const EventBus = new Vue();

(2)发送事件,假设有两个兄弟组件 firstCom 和 secondCom

firstCom 和 secondCom 的父组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div>
<first-com></first-com>
<second-com></second-com>
</div>
</template>

<script>
import firstCom from "./firstCom.vue";
import secondCom from "./secondCom.vue";
export default {
components: { firstCom, secondCom },
};
</script>

在 firstCom 组件中发送事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>
<button @click="add">加法</button>
</div>
</template>

<script>
import { EventBus } from "./event-bus.js"; // 引入事件中心

export default {
data() {
return {
num: 0,
};
},
methods: {
add() {
EventBus.$emit("addition", {
num: this.num++,
});
},
},
};
</script>

(3)接收事件
在 secondCom 组件中接收事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>求和: {{ count }}</div>
</template>

<script>
import { EventBus } from "./event-bus.js";
export default {
data() {
return {
count: 0,
};
},
mounted() {
EventBus.$on("addition", (param) => {
this.count = this.count + param.num;
});
},
};
</script>

在上述代码中,相当于将 num 值存储在事件总线中,在其他组件中可以直接访问。事件总线就相当于一个桥梁,不用组件通过它来通信。虽然看起来比较简单,但是这种方法也有不便之处,如果项目过大,使用这种方式进行通信,后期维护起来很困难。

4、依赖注入(provide/inject)

这种方式就是 vue 中依赖注入,该方法用于父子组件之间的通信。当然这里所说的父子不一定是真正的父子,也可以是祖孙组件,在层数很深的情况下,可以使用这种方式来进行传值,就不用一层一层的传递数据了。

provide/inject 是 vue 提供的两个钩子,和 data、methods 同级,并且 provide 的书写形式和 data 一样

  • provide 钩子用来发送数据或方法
  • inject 钩子用来接收数据或方法

用法
父组件中

1
2
3
4
5
6
7
8
9
<script>
export default {
provide() {
return {
num: this.num,
};
},
};
</script>

子组件中

1
2
3
4
5
<script>
export default {
inject: ["num"],
};
</script>

还有另一种写法,这种写法可以访问父组件中的所有属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
export default {
provide() {
app: this;
},
data() {
return {
num: 1,
};
},
inject: ["app"],
methods: {
test() {
console.log(this.app.num);
},
},
};
</script>

注意:依赖注入所提供的属性是非响应式的

5、$parent/$children

  • 使用$parent可以让组件访问父组件的实例(访问的是上一级父组件的属性和方法)
  • 使用$children可以让组件访问子组件的实例,但是$children并不能保证顺序,并且访问的数据也不是响应式的

用法
子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
<span>{{message}}</span>
<p>获取父组件的值为: {{parentVal}}</p>
</div>
</template>

<script>
export default {
data() {
return {
message: 'Vue'
}
},
computed:{
parentVal(){
return this.$parent.msg;
}
}
}
</script>

父组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<div class="hello_world">
<div>{{msg}}</div>
<child></child>
<button @click="change">点击改变子组件值</button>
</div>
</template>

<script>
import child from './child.vue'
export default {
components: { child },
data() {
return {
msg: 'Welcome'
}
},
methods: {
change() {
// 获取到子组件
this.$children[0].message = 'JavaScript'
}
}
}
</script>

在上面的代码中,子组件获取到了父组件的parentVal值,父组件改变了子组件中message的值

注意

  • 通过$parent访问到的是上一级父组件的实例,可以使用$root来访问根组件的实例
  • 在组件中使用$children拿到的是所有的子组件的实例,它是一个数组,并且是无序的
  • 在根组件#app上拿$parent得到的是new Vue()的实例,在这实例上再拿$parent得到的是undefined,而在最底层的子组件拿$children是个空数组
  • $children的值是数组,而$parent是个对象

6、$attrs/$listeners

考虑一种场景,如果A是B组件的父组件,B是C组件的父组件。如果想要组件A给C组件传递数据,这种隔代传数据的情况该使用哪种方式呢?
如果是用props/$emit来一级一级的传递,确实可以完成,但是比较复杂;如果使用事件总线,在多人开发或者项目较大的时候,维护起来很麻烦;如果使用vuex,如果仅仅是传递数据,那可能比较浪费。

  • $attrs:继承所有的父组件属性(除了props传递的属性、class和style),一般用在子组件的子元素上
  • $listeners:该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合v-on=”$listeners”将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)

inheritAttrs

  • 默认值为true,继承所有的父组件属性除props之外的所有属性
  • 只继承class属性

用法
A组件(app.vue)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div id="app">
//此处监听了两个事件,可以在B组件或者C组件中直接触发
<Child1 :p-child1="child1" :p-child2="child2" @test1="onTest1" @test2="onTest2"></Child1>
</div>
</template>
<script>
import Child1 from './Child1.vue';
export default {
components: { Child1 },
methods: {
onTest1() {
console.log('test1 running');
},
onTest2() {
console.log('test2 running');
}
}
};
</script>

B组件(Child1.vue)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div class="child-1">
<p>props: {{pChild1}}</p>
<p>$attrs: {{$attrs}}</p>
<Child2 v-bind="$attrs" v-on="$listeners"></Child2>
</div>
</template>
<script>
import Child2 from './Child2.vue';
export default {
props: ['pChild1'],
components: { Child2 },
inheritAttrs: false,
mounted() {
this.$emit('test1'); // 触发APP.vue中的test1方法
}
};
</script>

C组件(Child2.vue)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div class="child-2">
<p>props: {{pChild2}}</p>
<p>$attrs: {{$attrs}}</p>
</div>
</template>
<script>
export default {
props: ['pChild2'],
inheritAttrs: false,
mounted() {
this.$emit('test2');// 触发APP.vue中的test2方法
}
};
</script>

在上述代码中

  • C组件中能直接触发test的原因在于B组件调用C组件时,使用v-on绑定了$listeners属性
  • 在B组件中通过v-bind绑定$attrs属性,C组件中可以直接获取到A组件中传递下来的props(除了B组件中props声明的)

总结

根据以上对这6种组件间的通信方法,可以将不同组件间的通信分为4种类型:父子组件间通信、跨代组件间通信、兄弟组件间通信、任意组件间通信

1、父子组件间通信
  • 子组件通过 props 属性来接受父组件的数据,然后父组件在子组件上注册监听事件,子组件通过 emit 触发事件来向父组件发送数据
  • 通过 ref 属性给子组件设置一个名字。父组件通过 $refs 组件名来获得子组件,子组件通过 $parent 获得父组件,这样也可以实现通信
  • 使用 provide/inject,在父组件中通过 provide提供变量,在子组件中通过 inject 来将变量注入到组件中。不论子组件有多深,只要调用了 inject 那么就可以注入 provide中的数据
2、跨代组件间通信
  • 跨代组件间通信其实就是多层的父子组件通信,同样可以使用上述父子组件间通信的方法,只不过需要多层通信会比较麻烦
  • 使用上述的6种方法的$attrs / $listeners方法
3、兄弟组件间通信
  • 通过 $parent + $refs 以父组件为中间人来获取到兄弟组件,也可以进行通信
4、任意组件间通信
  • 使用 eventBus ,其实就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。它的本质是通过创建一个空的 Vue 实例来作为消息传递的对象,通信的组件引入这个实例,通信的组件通过在这个实例上监听和触发事件,来实现消息的传递

如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候采用上面这一些方法可能不利于项目的维护。这个时候可以使用 vuex ,vuex 的思想就是将这一些公共的数据抽离出来,将它作为一个全局的变量来管理,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。

3、Vue中key是用来做什么的?为什么不推荐使用index作为key?

1、key的作用主要是为了高效的更新虚拟DOM(使用key,会基于key的变化重新排列元素顺序,并且会移除key不存在的元素)

2、当以数组的下标index作为key值时,其中一个元素发生了变化就有可能导致所有元素的key值发生变化

4、Vue生命周期

从Vue实例创建、运行、到销毁期间,伴随着的各种事件,这些事件统称为生命周期

  • 创建期间的生命周期函数

    • beforeCreate:实例刚在内存中被创建出来,此时还没有初始化data和methods属性
    • ceated:实例已经在内存中创建出来,此时的data和methods以及创建完成,但是还没有开始编译模版
    • beforeMount:此时已经完成了模版的编译,但是还没有挂载到页面上
    • mounted:已经将编译好的模版挂载到了页面指定的容器中显示
  • 运行期间的生命周期函数

    • beforeUpdate:状态更新之前执行此函数,此时data中的状态值是最新的,但是界面上显示的数据还是旧的,因为此时还没有开始重新渲染DOM节点
    • updated:实例更新完毕后调用此函数,此时data中的状态值和界面上显示的数据,都已经完成更新,界面已经被重新渲染好了
  • 销毁期间的生命周期函数

  • 注:vue3中变更为onbeforeUnmount和onUnmounted

    • beforeDestory:实例销毁前调用,在这一步,实例仍然完全可用
    • destoryed:Vue实例销毁之后调用,调用后所有实例指示的东西都会解绑,所有的事件监听器会被移除,所有的子实例也会被销毁

通过一个例子看生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<body>
<div id="app">
<p>{{message}}</p>
<button @click="changeMsg">改变</button>
</div>
</body>
<script>
var vm = new Vue({
el: '#app',
data: {
message: 'hello world'
},
methods: {
changeMsg () {
this.message = 'goodbye world'
}
},
beforeCreate() {
console.log('------初始化前------')
console.log(this.message) // undefined
console.log(this.$el) // undefined
},
created () {
console.log('------初始化完成------')
console.log(this.message) // hello world
console.log(this.$el) // undefined
},
beforeMount () {
console.log('------挂载前---------')
console.log(this.message) // hello world
console.log(this.$el)
// 此时在内存中渲染好了模版,但是没有挂载到页面上去,大致就是下面这个状态
// <div id="app">
// <p>{{message}}</p>
// <button @click="changeMsg">改变</button>
// </div>
},
mounted () {
console.log('------挂载完成---------')
console.log(this.message) // hello world
console.log(this.$el.innerHTML) // <p>hello world</p><button>改变</button>
console.log(this.$el)
// 这时候把内存里渲染好的元素给到页面了,所以现在的插值语法里有内容了
// <div id="app">
// <p>hello world</p>
// <button>改变</button>
// </div>
},
beforeUpdate () {
console.log('------更新前---------')
console.log(this.message) // goodbye world
console.log(this.$el.innerHTML) // <p>hello world</p><button>改变</button>
console.log(this.$el)
// <div id="app">
// <p>goodbye world</p>
// <button>改变</button>
// </div>
},
updated() {
console.log('------更新后---------')
console.log(this.message) // goodbye world
console.log(this.$el.innerHTML) // <p>goodbye world</p><button>改变</button>
console.log(this.$el)
// <div id="app">
// <p>goodbye world</p>
// <button>改变</button>
// </div>

// 打印发现更新前和更新后出现的el都一样,很奇怪,这并不符合两个钩子的描述
// 实际上是因为this.$el是一个对象,本质就是一个指针,当我们刚console.log输出的时候,
// 其实并没有显示内容,而当我们点击箭头去展开这个div的时候,将指针指向了当前的$el,所以我们看到的才会都是改变后的$el
// 那就用.innerHtml来验证一下

// 验证后就发现确实符合两个钩子的描述了
}
})

</script>

5、v-show和v-if区别

v-show原理是修改元素的css属性display:none来决定是否隐藏,DOM元素依旧存在

v-if是通过操作DOM进行切换显示,将DOM整个添加或删除

6、双向数据绑定

实现mvvm的双向绑定,采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter、getter,在数据发生变动时发布消息给订阅者,触发相应的监听回调来渲染视图。

1、什么是setter、getter
get对应的方法称为getter,负责获取值,它不带任何参数。set对应的方法为setter,负责设置值,在它的函数体中,一切的return都是无效的。

2、什么是Object.defineProperty()
对象是由多个键值对组成的无序的集合,对象中每个属性对应任意类型的值
定义对象可以使用构造函数或字面量形式

举例
1
2
3
let obj = new Object  // obj={}
obj.name ="张三" // 添加描述
obj.say = function(){} // 添加行为

当然还可以使用Object.defineProperty定义新属性或修改原有的属性

语法 Object.defineProperty(obj, prop, descriptor)
参数
obj:必需。目标对象;
prop:必需。需定义或修改的属性的名字;
descriptor:必需。目标属性所拥有的特性;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let obj = {}
Object.defineProperty(obj, 'name', {
get: function(){
console.log('被获取了')
return val
},
set: function(){
console.log('被设置了')
}
})
obj.name = '张三'
// 此时触发set方法
// 打印 被设置了
let val = obj.name
// 此时获取obj的name属性,触发get方法
// 打印 被获取了

实现一个low版双向绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let obj = {}
let demo = document.querySelector('#demo')
let input1 = document.querySelector('#input1')
Object.defineProperty(obj, 'name', {
get: function(){
return val
},
set: function(newVal){
input1.value = newVal
demo.innerHTML = newVal
}
})
input1.addEventListener('input', function(e){
obj.name = e.target.value
})

简单实现一个js的双向绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<input type="text" id="input1"/>
输入的值为:<span id="out"></span>
<script>
var input1 = document.getElementById('input1');
var out = document.getElementById('out');
var obj = {};
Object.defineProperty(obj, 'msg', {
enumerable: true,
configurable: true,
set (newVal) {
out.innerHTML = newVal;
}
})
input1.addEventListener('input', function(e) {
obj.msg = e.target.value;
})
</script>

这样就实现了js的双向数据绑定,随着文本框输入文字的变化,span中会同步显示相同的文字内容;这样就实现了 model => view 以及 view => model 的双向绑定。
通过给in输入框添加事件监听input来触发obj对象的set方法,而set再修改了访问器属性的同时,也修改了dom样式,改变了span标签内的文本。

7、Vue路由守卫的钩子函数

全局路由守卫(写在router文件夹的index.js中)

  • router.beforeEach:全局前置守卫,进入路由之前触发
  • router.beforeResolve:全局解析守卫,在beforeRouterEnter调用后调用
  • router.afterEach:全局后置守卫,进入路由后触发

路由组件守卫(写在.vue文件里)

  • beforeRouterEnter:进入组件之前触发,在Created前面
  • beforeRouterUpdated:路由更新但是内容不会改变
  • beforeRouterLeave:离开之前触发,在beforeDestory之前触发

路由独享守卫(写在router文件夹的index.js中,routes里的某个路由单独使用)

  • beforeEnter:读取路由的信息,独享路由守卫只有前置没有后置

8、vue编程式、声明式导航跳转传参方式有哪些?

1、编程式传参

1.1、路由命名:name+params

router.push({name: 路由规则的路由名字, params: {键值对}})

1.2、查询参数:path+query

router.push({path: 路由规则的路由参数, query: {键值对}})

2、声明式传参

1.1、路由命名:name+params组合

<router-link :to=”{name: 路由规则的路由名字 , params: {键值对}}”>click to new page</router-link>

1.2、查询参数:path+query组合

<router-link :to=”{ path: 路由规则的路由参数, query: {键值对}}”>click to news page</router-link>

9、vuex是什么?怎么使用?哪种功能场景会使用它?

1、vuex就是一个仓库,仓库里放了很多对象,其中state就是数据源存放地,对应一般vue对象里的data
2、state里存放的数据是响应式的,vue组件从store读取数据,若是store中的数据发生改变,依赖这项数据的组件也会发生更新
3、它通过mapState把全局的state和getters映射到当前组件的computed计算属性

vuex里有哪些属性?
state、getters、mutations、actions、modules
state:存放数据的地方,类似于vue文件中的data,数据可以共享,所有组件都可以使用,但是不能直接对state里的数据进行修改
getters:类似于vue的计算属性,主要用来过滤数据,组件可以通过此方法获取想要的数据
mutations:在这里定义的方法动态修改vuex中state数据
actions:可以理解为把mutations里处理数据的方法变成可异步的处理数据的方法,简单来说就是异步操作数据。view层通过store.dispatch分发action

vuex一般用于中大型web单页面应用中对应用的状态进行管理,对于一些组件间关系较为简单的小型应用,使用vuex的必要性不是很大,因为完全可以用组件prop属性或者事件来完成父子组件间的通信,vuex更多用于解决跨组件通信以及作为数据中心集中存储数据

使用vuex解决非父子组件之间的通信问题
vuex是通过将state作为数据中心,各个组件共享state实现跨组件通信的,此时的数据完全独立于组件,因此将组件间共享的数据置于state中能有效解决多层级组件嵌套的跨组件通信问题

vuex作为数据存储中心
vuex的state在单页面应用的开发中本身具有一个数据库的作用,可以将组件中用到的数据存储在state中,并在action中封装数据读写的逻辑,这时候存在一个问题,一般什么样的数据会放在state中?目前主要有两种数据交给vuex管理:

1.组件间全局共享的数据
2.通过后端异步请求的数据

比如做加入购物车、登录状态等都能使用vuex来管理数据状态

vuex页面刷新数据丢失问题的解决方式

有四种解决方法来使vuex数据持久化
1、使用sessionStorage,在将数据存储至store的同时,也要保存至sessoinStorage。需要注意的是vuex是响应式的,但是浏览器存储不是,所以state需要从sessionStorage里获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default new Vuex.store({
state:{
authInfo: JSON.parse(sessionStorage.getItem("COMPANY_AUTH_INFO")) || {}
},
getters:{
authInfo: state => state.authInfo
},
mutations:{
SET_COMPANY_AUTH_INFO(state, data) {
state.authInfo = data
sessionStorage.setItem("COMPANY_AUTH_INFO", JSON.stringify(data))
}
},
actions:{
},
modules:{
}

})

2、使用vuex-along,首先npm install vuex-along –save,然后进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Vue from 'vue'
import Vuex from 'vuex'
import indexOne from "./modules/indexOne"
import VueXAlong from 'vuex-along'

Vue.use(Vuex)
export default new Vuex.Store({
strict: false,
modules:{
indexOne
},

plugins: [VueXAlong({
name: 'along', //存放在localStroage或者sessionStroage 中的名字
local: false, //是否存放在local中 false 不存放 如果存放按照下面session的配置配
session: { list: [], isFilter: true }
//如果值不为false 那么可以传递对象 其中 当isFilter设置为true时, list 数组中的值就会被过滤调,这些值不会存放在seesion或者local中
})]

})

3、vuex-persistedstate(本人常用),首先执行npm install –save vuex-persistedstate,然后进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'

Vue.use(Vuex)

export default new Vuex.Store({
state: {
},
getters: {
},
mutations: {
},
actions: {

},
modules: {
},
plugins:[createPersistedState({
storage:window.sessionStorage
})]
})

4、vuex-persist,同样执行npm install –save vuex-persist or yarn add vuex-persist,然后如下进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import Vue from 'vue'
import Vuex from 'vuex'
import indexOne from "./modules/indexOne"
import VuexPersistence from 'vuex-persist'
Vue.use(Vuex)

const vuexLocal = new VuexPersistence({
storage: window.localStorage
})

export default new Vuex.Store({
strict: false,
modules:{
indexOne,
},
plugins: [vuexLocal.plugin]

})

10、MVC和MVVM的区别

MVC

MVC包括view视图层、model数据层、controller控制层,各部分之间的通信是单向的

view传送指令到controller层,controller完成业务逻辑后,要求model数据层改变状态,model将新的数据发送到view,用户得到反馈

MVVM

MVVM包括view视图层、model数据层、viewmodel视图模型层,各部分都是双向的。
采用双向数据绑定,如此一来,当view视图层发生变动时,就会自动更新到viewmodel层,反之亦然。
MVVM代表框架:Angular、React、Vue

MVVM与MVC的最大区别就是:它实现了View和Model的自动同步,也就是当Model的数据改变时,我们不用再自己手动操作Dom元素,来改变View的显示,而是改变数据后该数据对应View层显示会自动改变。它解决了MVC中大量操作DOM导致页面渲染性能降低,加载速度变慢的问题。

但是要知道,MVVM并不是用VM完全取代了C,ViewModel存在目的在于抽离Controller中展示的业务逻辑,而不是替代Controller,其它视图操作业务等还是应该放在Controller中实现。

11、说出至少vue的3个常用事件修饰符

  • .stop:阻止点击事件冒泡,如果子元素设置@click.stop就不会触发父元素的click事件
  • .prevent:阻止默认事件,比如a标签设置@click.prevent就不会进行路由跳转
  • .self:只在元素本身触发,防止事件冒泡。如果父元素设置@click.self就不会被子元素的click影响
  • .once:事件只触发一次
  • .passive:滚动事件的默认行为 (即滚动行为) 将会立即触发,不能和.prevent 一起使用,浏览器内核线程在每个事件执行时查询prevent,造成卡顿,使用passive将会跳过内核线程查询,进而提升流畅度
  • .capture:对于冒泡事件,且存在多个冒泡事件时,存在该修饰符的会优先执行,如果有多个,则从外到内执行
  • .native:将vue组件转换为一个普通的HTML标签,如果该修饰符用在普通html标签上是不起任何作用的

12、v-if和v-for的优先级问题

1、作用
v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 true值的时候被渲染

v-for 指令基于一个数组来渲染一个列表。v-for 指令需要使用 item in items 形式的特殊语法,其中 items 是源数据数组或者对象,而 item 则是被迭代的数组元素的别名。在 v-for 的时候,建议设置key值,并且保证每个key值是独一无二的,这便于diff算法进行优化

用法示例
1
2
3
4
<div v-if="isShow"></div>
<li v-for="item in items" :key="item.id">
{{ item.label }}
</li>

2、优先级
v-for优先级高于v-if
所以如果将两者放在同级标签(如下所示),那么每次v-for都在执行v-if,造成不必要的性能浪费,尤其是当前只需要渲染很小一部分的时候

1
2
3
4
5
<ul>
<li v-for="user in users" v-if="user.isActive" :key="user.id">
{{ user.name }}
</li>
</ul>

如上情况,即使有很多user 但是只要有一个需要使用v-if,也需要循环整个数组,这在性能上是极大的浪费。

所以vue2推荐我们使用computed计算属性来解决这一问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
<div>
<div v-for="(user,index) in activeUsers" :key="user.index" >
{{ user.name }}
</div>
</div>
</template>
<script>
export default {
data(){ // 业务逻辑里面定义的数据
return {
users,: [{
name: '111111',
isShow: true
}, {
name: '22222',
isShow: false
}]
}
},
computed: {
activeUsers: function () {
return this.users.filter(function (user) {
return user.isShow;//返回isShow=true的项,添加到activeUsers数组
})
}
}
}
</script>

总结:vue2中要尽量避免v-for和v-if同时使用,如果要处理类似情况,可以选择使用computed过滤掉不需要显示的项目。

我们上面一直都在说vue2中v-for优先级高于v-if。那么vue3是不是就不一样了呢?
没错,在vue3中,鱿鱼须对调了两者,使得v-if的优先级更高。不过同样,当同时使用它们的时候优先级不明显,所以vue官方仍不建议一起使用

下列内容摘自vue3官方文档

当它们同时存在于一个节点上时,v-if比v-for的优先级更高。这意味着v-if的条件将无法访问到v-for作用域内定义的变量别名:

1
2
3
4
5
6
7
<!--
这会抛出一个错误,因为属性 todo 此时
没有在该实例上定义
-->
<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo.name }}
</li>

在外新包装一层<template> 再在其上使用v-for可以解决这个问题 (这也更加明显易读):

1
2
3
4
5
<template v-for="todo in todos">
<li v-if="!todo.isComplete">
{{ todo.name }}
</li>
</template>

当你使用

1
2
3
<template v-for="todo in todos" :key="todo.name">
<li>{{ todo.name }}</li>
</template>

13、vue2和vue3的区别

1、双向数据绑定的方式不同

vue2采用es5的Object.defineProperty()对数据进行劫持,结合发布者-订阅者模式进行
vue3采用es6的proxy来进行数据代理,修复了vue2中对象和数组的属性添加修改的问题

2、根结点数量不同

vue2中组件只能有一个根节点
vue3中可以有多个根节点,解决了多个div嵌套的问题

3、vue3中增加了Composition API(组合api)

vue2中使用Options API,这种写法不方便我们的阅读和交流,逻辑过于分散

在vue2中定义数据变量是data(){},创建的方法要在methods:{}中
vue3中直接在setup(){}中(也可以使用语法糖,在script中加上setup即可),在这里面定义的变量和方法因为最终要在模板中使用,所以最后都得return。

4、生命周期的变化

vue3没有beforeCreated和created,取而代之的是setup()

  • beforeMounted => onBeforeMounted
  • mounted => onMounted
  • beforeUpdate => onBeforeUpdate
  • updated => onUpdated
  • beforeDestory => onBeforeUnmount
  • destoryed => onUnmounted
  • activated => onActivated
  • deactivated => onDeactivated

vue3生命周期在调用前需要先进行引入

5、vue2和vue3的diff算法

vue2的diff算法就是进行虚拟节点对比,并返回一个patch对象,用来存储两个节点不同的地方,最后用patch记录的消息去局部更新DOM。

vue3 diff算法在初始化的时候会给每个虚拟节点添加一个patchFlags,patchFlags就是优化的标识。
只会比较patchFlags发生变化的VN哦的,进行更新视图,对于没有变化的元素做静态标记,在渲染的时候直接复用。

6、v-if和v-for的优先级

vue2中最好不要把v-if和v-for同时用在一个元素上,这样会带来性能浪费(每次都要先渲染才会进行条件判断)v-for优先于v-if生效

vue3中v-if优先级优于v-for生效
vue中会给我们报警告,意思是属性index在渲染期间被访问,但未在实例上定义(v-if先进性判断,但是这时候v-for还没有渲染,所以index是找不到的)

v-if和v-for的优先级会在第12点详细描述

7、vue3打包点时候无用代码丢弃

14、为什么vue中data要写成函数?

  • vue中组件是用来复用的,为了防止data复用,将其定义为函数。
  • vue组件中的data数据都应该是相互隔离,互不影响的,组件每复用一次,data数据就应该被复制一次,之后,当某一处复用的地方组件内data数据被改变时,其他复用地方组件的data数据不受影响,就需要通过data函数返回一个对象作为组件的状态
  • data是一个函数的话,每复用一次组件,就会返回一份新的data(类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护自己的数据,不会造成数据污染)
  • 当我们组件的date单纯的写成对象形式,这些实例用的是同一个构造函数,由于JavaScript的特性所导致,所有的组件实例共用了一个data,就会造成一个变了全都会变的结果

15、vue2解决数组更新

待解决问题如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>
<h2>数组展示:{{testArr}}</h2>
<button @click="changeArr">修改数组</button>
</div>
</template>

<script>
export default {
name: "fun",
data: function() {
return {
testArr:["a","b","c","d"]
}
},
methods: {
changeArr(){
// 这样直接修改数组是没用的,页面不会重新渲染
this.testArr[1] = "w"
console.log(this.testArr)
}
}
}
</script>

方法一
使用this.$set,此方法也可以给对象新增属性,并更新视图

1
2
3
4
5
changeArr(){
//第一种方法: 用set来更新数据
this.$set(this.testArr, 1, "Q")
console.log(this.testArr)
}

方法二
数组splice方法

1
2
3
4
5
changeArr(){
// 第二种方法 splice
this.testArr.splice(1, 1, "R")
console.log(this.testArr)
}

方法三
this.$forceUpdate()强制刷新

1
2
3
4
5
6
changeArr(){
// 第三种方法: this.$forceUpdate()强制刷新
this.testArr[1] = "G"
this.$forceUpdate();
console.log(this.testArr)
}

方法四
扩展运算符

1
2
3
4
5
6
changeArr(){
// 第四种方法: es6 "..."扩展运算符
this.testArr[1] = "Y"
this.testArr = [...this.testArr]
console.log(this.testArr)
}

16、computed和watch的区别

首先computed和watch都是用来监听数据变化的方式,但是它们的实现方式和应用场景不同。

  • computed是计算属性,根据已有的数据计算出新的值,并且能够将其缓存起来,只有相关数据发生改变时才会重新计算。computed适用于依赖多个数据计算的场景,比如过滤和排序等
  • watch是一个监听器,能够监听指定数据的变化,并在数据变化时执行相应的回调函数。watch适用于需要对数据进行复杂处理或者进行异步操作的场景,比如网络请求和动画效果等
    使用哪个应根据实际情况来决定

17、vue3中ref和reactive的区别?

ref用于创建一个可以在组件之间共享的响应式变量,可以在模版中直接访问,也可以在计算属性和方法中使用
reactive是一个辅助函数,用来将对象或者数组转换为响应式对象,可以在组件中维护状态,并在状态更改时触发视图更新

ref用于创建单个响应式变量,所以一般适合声明基本的基本数据类型(引用数据类型也可以)
reactive用于创建一个响应式对象,所以一般适合声明对象或者数组等引用数据类型

像vue2中在data里定义数据是很方便的,而vue3中我们还要在setup里返回我们定义的数据,那么为什么vue3感觉上麻烦呢?
因为vue2里data的响应式是有缺陷的,我们知道面试里经常会问vue2和vue3的双向绑定的区别。vue2中增加和删除对象、数组里的数据时,无法捕获到变化就不会去更新视图,因为vue2的响应式使用的是Object.defineProperty,而vue3我们定义一个对象类型的数据时使用的是reactive,返回的是一个proxy代理对象,proxy内部是可以检测到删除和增加的,因此响应式相比vue2更强大了

ref可以理解为reactive的再封装,如果是对象类型的数据,底层还是reactive的逻辑,我们知道,使用ref定义基本数据类型时,在脚本里使用时,需要加.value后缀,而在模版里不需要,vue3会自动补充

总结
1、ref用于定义基本类型和引用类型,reactive仅用来定义引用数据类型(如果出现reactive(‘我是reactive’),vue3会警告value cannot be made reactive)
2、reactive只能用于定义引用数据类型的原因在于内部是通过ES6的Proxy实现响应式的,而Proxy不适用于基本数据类型
3、ref定义对象时,底层会通过reactive转换成具有深层次的响应式对象,所以ref本质上是reactive的再封装
4、在脚本里使用ref定义的数据时,要加上.value,模版中使用则不需要
5、定义数组建议使用ref,避免reactive定义值修改导致的响应式丢失问题(两者均可定义数组,解决响应式问题见tips)

ref和reactive定义数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 第一种
const tableData = ref([])
const getData = async ()=>{
const {data} = await getDataApi()
tableData.value = data
}

// 第二种
const tableData = reactive([])
const getData = async ()=>{
const {data} = await getDataApi()
tableData.push(...data)
}

// 第三种
const tableData = reactive({array:[]})
const getData = async ()=>{
const {data} = await getDataApi()
tableData.array = data
}

// 第四种
const tableData = reactive([])
const getData = async ()=>{
const {data} = await getDataApi()
tableData = reactive(data)
}

—HTTP 部分—

1、http协议

超文本传输协议,超文本可以说是“超级文本”或者说是“带超链接文本”。超链接文本可以有图片、动图、文字、视频。从本质上说它是一个内容文本,我们对网站的浏览,实际上是对内容的浏览。对于这些内容,都有统一的路径,我们称之为URL地址。

HTTP协议是可靠的数据传输协议。

可靠性是依赖于传输层的TCP协议来实现的。也就是说,HTTP协议的底层是TCP协议通过TCP协议的可靠性从而保证HTTP协议也是可靠的。
数据包括文本、图片、文件、动图、视频、音频。这些构成了web网站内容,我们平时都是对这些内容进行浏览。

2、说一下http和https

http:超文本传输协议,是一个客户端和服务器端请求和应答的标准(TCP)
https:是以安全为目标的http通道,即http下加入SSL层,比http更安全

区别:
安全性和资源消耗: HTTP协议运行在TCP之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS是运行在SSL/TLS之上的HTTP协议,SSL/TLS 运行在TCP之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS高,但是 HTTPS 比HTTP耗费更多服务器资源。

对称加密:密钥只有一个,加密解密为同一个密码,且加解密速度快,典型的对称加密算法有DES、AES等。

非对称加密:密钥成对出现(且根据公钥无法推知私钥,根据私钥也无法推知公钥),加密解密使用不同密钥(公钥加密需要私钥解密,私钥加密需要公钥解密),相对对称加密速度较慢,典型的非对称加密算法有RSA、DSA等。

3、get和post请求的区别

1、url可见性
get,参数url可见
post,url参数不可见

2、数据传输上
get,可以通过拼接url进行参数传递
post,通过body体传输参数

3、缓存性
get请求可以缓存
post请求不可以缓存

4、后退页面反应
get不会产生影响
post则会重新提交请求

5、传输数据的大小
get一般传输不超过2-4k
post请求传输数据可以自行设置,也可以无限大

6、安全性
这个也是最不好分析的,原则上post肯定要比get安全,毕竟传输参数时url不可见,但也挡不住部分人闲的没事在那抓包玩。安全性个人觉得是没多大区别的,防君子不防小人就是这个道理。对传递的参数进行加密,其实都一样。

7、数据包
get产生一个tcp数据包,对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据)
POST产生两个TCP数据包,对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)

在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。

4、三次握手和四次挥手

1、三次握手

建立一个tcp链接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力是否正常、指定自己的初始化序列号为后面的可靠性传输做准备。实质上其实就是连接服务器指定端口,建立tcp连接,并同步连接双方的序列号和确认号,交换tcp窗口大小信息

刚开始客户端处于closed状态,服务端处于listen状态
进行三次握手:

第一次握手:客户端给服务端发一个SYN报文,并指明客户端的初始化序列号ISN(c),此时客户端处于SYN_SENT状态
首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号

第二次握手:服务器收到客户端的SYN报文之后,会以自己的SYN报文作为应答,并且也是指定了自己的初始化序列号ISN(s)。同时会把客户端的ISN+1作为ACK的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态
在确认报文段中SYN=1,ACK=1,初始序号seq=y,确认号ack=x+1

第三次握手:客户端收到SYN报文后,会发送一个ACK报文,当然,也是一样把服务器的ISN+1作为ACK的值,表示已经收到了服务端的SYN报文,此时客户端处于ESTABLISHED状态。服务器收到ACK报文之后,也处于ESTABLISHED状态,此时,双方已建立起了连接
确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号

2、四次挥手

建立一个连接需要三次握手,而终止一个连接要经过四次挥手(也有将四次挥手叫做四次握手的)。这由TCP的半关闭(half-close)造成的。所谓的半关闭,其实就是TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力

TCP 连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),客户端或服务端均可主动发起挥手动作

刚开始双方都处于ESTABLISHED状态,假如是客户端先发起关闭请求。
四次挥手的过程:

第一次挥手:客户端发送一个FIN报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT-1状态
即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认

第二次挥手:服务器收到FIN后,会发送一个ACK报文,且把客户端的序列号值+1作为ACK报文的序列号值,表明已经收到客户端的报文,此时服务端处于CLOSE_WAIT状态
即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态。此时的TCP处于半关闭状态,客户端到服务端到连接释放。客户端收到服务端的确认后,进入FIN_WAIT-2状态,等待服务端发出的连接释放报文段

第三次挥手:如果服务端也想断开连接了,和客户端第一次挥手一样,服务端会向客户端发送FIN报文,且指定一个序列。此时服务端处于LAST_ACK状态
即服务端没有要向客户端发送数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK,等待客户端确认

第四次挥手:客户端收到FIN报文,发送一个ACK报文作为应答,且把服务端的序列号值+1作为ACK的序列号值,此时客户端处于TIME_WAIT状态,需要过一阵子以确保服务端收到自己的ACK报文后才会进入CLOSED状态,服务端收到ACK报文后立即关闭连接,进入CLOSED状态
即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态

截图均来自up主掌芝士ss的视频

5、浏览器输入一个url到页面展示,中间发生了什么?

1、浏览器输入url,解析url地址是否合法
2、浏览器检查是否有缓存(浏览器缓存-系统缓存-路由缓存)如果有,直接显示,没有继续往下
3、发送http请求前需要域名解析(DNS解析),解析获取对应的ip地址
4、浏览器向服务器发起tcp链接,建立tcp连接
5、三次握手成功后,浏览器向服务器发送http请求,请求数据包
6、服务器收到处理的请求,将数据返回至浏览器
7、浏览器收到http响应
8、浏览器解析响应,如果响应可以缓存,则存入缓存
9、浏览器发送请求获取嵌入html的资源(html、css、js、图片等等资源)
10、浏览器发送异步请求
11、页面全部渲染结束

6、常用的http方法有哪些?

  • GET 从服务端获取数据
  • POST 向服务端发送待处理的数据
  • PUT 向服务端发送数据并替换服务端上的指定数据
  • HEAD 从服务端获取指定信息的头部
  • OPTIONS 查询针对请求url指定的资源支持
  • DELETE 从服务端删除置顶数据
  • TRACE 沿着目标资源的路径执行消息换回测试

7、http常见状态码

1、1**的状态码(信息类)

100,接收的请求正在处理

2、2**的状态码(成功类)

2**(成功)表示成功处理了请求的状态码
200(成功)服务器已成功处理了请求

3、3**的状态码(重定向)

3**(重定向)表示要完成请求,需要进一步操作。通常这些状态代码用来重定向
301永久性重定向,表示资源已被分配了新的URL
302临时性重定向,表示资源临时被分配了新的URL
303表示资源存在另一个URL,用GET方法获取资源
304(未修改)自从上次请求后,请求网页未修改过。服务器返回此响应时,不会返回网页内容

4、4**的状态码(客户端错误)

4**(请求错误)这些状态码表示请求可能出错,妨碍了服务器的处理
400(错误请求)服务器不理解请求的语法
401表示发送的请求需要有通过HTTP认证的认证信息
403(禁止)服务器拒绝请求
404(未找到)服务器找不到请求网页

5、5**的状态码(服务器错误)

5**(服务器错误)这些状态码表示服务器在尝试处理请求时发生内部错误。这些错误可能是服务器本身的错误,而不是请求的错误
500(服务器内部错误)服务器遇到错误,无法完成请求
503表示服务器处于停机维护或超负载,无法处理请求

8、TCP和UDP的区别

UDP TCP
是否连接 无连接 面向连接
是否可靠 不可靠传输,不使用流量控制和拥塞控制 可靠传输,使用流量控制和拥塞控制
连接对象个数 支持一对一,一对多,多对一和多对多交互通信 只能是一对一通信
传输方式 面向报文 面向字节流
首部开销 首部开销小,仅8字节 首部最小20字节,最大60字节
适用场景 适用于实时性较高的应用(IP电话、直播、视频会议等) 适用于要求可靠精确无误传输的应用,文件传输,发送邮件等

—其他—

1、前端优化

1、减少请求数量

1.1图片处理
1.1.1 雪碧图

雪碧图是根据css sprite音译过来的,就是将很多小图标放在一张图片上就称之为雪碧图,可以减少网站http请求数量,但是当整合图片比较大的时候,一次加载比较慢,随着字体图片、svg图片的流行该技术慢慢退出了舞台

1.1.2 Base64

将图片的内容以Base64格式内嵌到HTML中,可以减少http请求数量,但是编码之后的大小比图片大了

1.1.3 使用字体图标来代替图片
1.2 减少重定向

尽量避免使用重定向,当页面发生了重定向,就会延迟整个HTML文档的传输。在HTML文档到达之前,页面中不会呈现任何东西,也没有任何组件会被下载,降低了用户体验

如果一定要使用重定向的话,如http重定向到https,要使用301永久重定向,而不是302临时重定向,因为如果使用302则每一次访问http都会重定向到https页面,而永久重定向在第一次从http重定向到https之后,每次访问http,会直接返回https的页面

1.3 使用缓存

使用cache-control或expires这类强缓存的时候,缓存不过期的情况下不会向服务器发起请求。强缓存过期的时候,会使用last-modified或etag这类协商缓存向服务器发起请求,如果资源没有变化,则服务器返回304响应,浏览器继续从本地缓存加载资源,如果资源更新了,则服务器将更新后的资源发送到浏览器,并返回200

1.4 不使用css@import

使用css@import会造成额外的请求

1.5 避免使用空的src和href

a标签设置空的href,会重定向到当前页面的地址
form设置空的method,会提交表单到当前页面的地址

2、减少资源大小

2.1 html压缩

html代码压缩就是压缩在文本文件中有意义,但是在html中不显示的字符,包括空格,制表符

2.2 css压缩

css压缩包括无效代码删除与css语义合并

2.3 js压缩与混乱

js压缩与混乱包括无效字符及注释的删除、代码语义的缩减和优化、降低代码的可读性、实现代码的保护

2.4 图片压缩

3、优化网络连接

3.1 使用CDN

CDN是内容分发网络,它能够实时地根据网络流量和各个节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上,其目的是使用户可以就近的取得所需内容,解决网络拥挤的状况,提高网站的响应速度

3.2 使用DNS预解析

当浏览器访问一个域名的时候,需要解析一次DNS,获得对应域名的ip地址,在解析过程中,按照浏览器缓存、系统缓存、路由器换算、DNS缓存、域名服务器的顺序,逐步读取缓存,直到拿到ip地址

3.3 持久连接

使用keep-alive或者persistent来建立持久连接,降低了延时和连接建立的开销

4、优化资源加载

4.1 资源加载位置

通过优化资源加载位置,更改资源加载时机,使尽可能快地展示出页面内容,尽可能快地使用功能可用
1、css文件放在head中,先外链,后本页
2、js文件放在body底部,先外连,后本页
3、处理页面、处理页面布局的js文件放在head中,如babel-polyfill.js文件、flexible.js文件
4、body中尽量不写style标签和script标签

4.2 资源加载时机

1、异步script标签
defer:异步加载,在html解析完成后执行。defer的实际效果与将代码放在body底部类似
async:异步加载,加载完成后立即执行
2、模块按需加载
在SPA等业务比较复杂的系统中,需要根据路由来加载当前页面所需要的业务模块
按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积

webpack提供了两类技术,优先选择的方式是使用符合ECMAScript提案的import语法,第二种就是使用webpack特定的require.ensure

3、使用资源预加载preload和资源预读取prefetch
preload让浏览器提前加载指定资源,需要执行时候再执行,可以加快当前页面的加载速度
prefetch告诉浏览器加载下一个页面可能会用到的资源,可以加速下一个页面的加载速度
4、资源懒加载与资源预加载
资源延迟加载也称为资源懒加载,延迟加载资源或符合某些条件的时候才加载某些资源
资源预加载是提前加载用户所需的资源,保证良好的用户体验
资源懒加载和资源预加载都是一种错峰操作,在浏览器忙碌的时候不能操作,浏览器空闲的时候再加载资源,优化了网络性能

5、减少重绘回流

6、性能更好的API

1、用对选择器

id选择器(#myid)
类选择器(.myclassname)
标签选择器(div,h1,p)
相邻选择器(h1+p)
子选择器(ul > li)
后代选择器(li a)
通配符选择器(*)
属性选择器(a[rel=“external”])
伪类选择器(a:hover,li:nth-child)

2、使用requestAnimationFrame来替代setTimeout和setInterval
希望在每一帧开始的时候对页面进行更改,requestAnimationFrame就是告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行,使用setTimeout或者setInterval来触发更新页面的函数,该函数可能在一帧的中间或者结束的时间点上调用,进而导致该帧后面需要进行的事情没有完成,引发丢帧

3、使用IntersectionObserver来实现图片可视区域的懒加载

传统的做法中,需要使用scroll事件,并调用getBoundingClientRect方法,来实现可视区域的判断,即使使用了函数节流,也会造成页面回流。使用IntersectionObserver,则没有上述问题

7、webpack性能优化

7.1 打包公共代码

使用CommonChunkPlugin插件,将公共模块拆出来,最终合成的文件能够在最开始的时候加载一次,便存到缓存区中供后续使用,这回带来速度上的提升,因为浏览器会迅速将公共的代码从缓存中抽取出来,而不是每次访问一个页面的时候,都需要去加载一个很大的文件
webpack 4 将移除 CommonsChunkPlugin, 取而代之的是两个新的配置项 optimization.splitChunks 和 optimization.runtimeChunk
通过设置 optimization.splitChunks.chunks: “all” 来启动默认的代码分割配置项

7.2 动态导入和按需加载

webpack提供了两种技术通过模块内联函数用来分离代码,优先选择的方式是ECMAScript提案的import()语法,第二种则是使用webpack特定的require.ensure

7.3 删除无用的代码

tree shaking是一个术语,通常用于移除Javascripy上下文中的未引用代码,它依赖于ES2015模块系统中的静态结构特性,例如import和export,js的tree shaking主要通过uglifyjs来完成,css的tree shaking通过purify css来实现

7.4 长缓存优化

1、将hash替换成chunkhash,这样当chunk不变的时候,缓存依然有效
2、使用Name而不是id

每个 module.id 会基于默认的解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变

下面来使用两个插件解决这个问题。第一个插件是 NamedModulesPlugin,将使用模块的路径,而不是数字标识符。虽然此插件有助于在开发过程中输出结果的可读性,然而执行时间会长一些。第二个选择是使用 HashedModuleIdsPlugin,推荐用于生产环境构建

7.5 公共代码内联

使用html-webpack-inline-chunk-plugin插件将manifest.js内联到html文件中

—JS代码输出和手写代码题—

本章不会对所写代码的相关概念进行详细解释(如果有涉及概念就以注释形式写在代码中),本章内容全部为面试可能会遇到的代码执行题或者手写实现题,输出结果通过注释表示。所有代码可以执行使用,建议动手敲一遍执行以便更加熟悉

手写递归拼接树
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
let list=[
{id:1001,parent:null,text:"菜单1"},
{id:1002,parent:1001,text:"菜单1-1"},
{id:1003,parent:1001,text:"菜单1-2"},
{id:1004,parent:null,text:"菜单2"},
{id:1005,parent:1004,text:"菜单2-1"},
{id:1006,parent:null,text:"菜单3"},
{id:1007,parent:1002,text:"菜单1-1-1"},
]
// 递归构建树结构
// 传入参数分别为原始数据,节点id,空数组
function listToTree(list,id,tree){
for(let item of list){
if(item.parent == id){
tree.push(item)
}
}
for(i of tree){
// 递归调用
i.children = listToTree(list,i.id,[])

if(i.children.length==0){
delete i.children
}
}
return tree
}
const res = listToTree(list,null,[])
console.log("res",res)

// 输出结果
// 直接在vscode输出里显示的所以children只能看到object,在浏览器控制台里就能看到完整的树结构
// [
// {
// id: 1001,
// parent: null,
// text: '菜单1',
// children: [ [Object], [Object] ]
// },
// { id: 1004, parent: null, text: '菜单2', children: [ [Object] ] },
// { id: 1006, parent: null, text: '菜单3', children: [] }
// ]

JS执行机制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 在js章节中认识到了js中的同步任务和异步任务机制,知道同步任务-微任务-宏任务的执行顺序,那么就可以出一些复杂的执行顺序题了,在事件循环一章中已经通过一道题目进行了练习,这里再提供一道更加复杂的类型
// 第一阶段 同步任务输出1
console.log(1)
// 第一阶段 宏任务 等待
// 第二阶段 继续等待
// 第三阶段 处理定时器宏任务 定时器内部 直接console和Promise主体为同步任务 按照 同步-微-宏顺序执行 输出2、3、4 目前结果1、5、6、2、3、4
setTimeout(() => {
console.log(2)
new Promise((resolve)=>{
console.log(3)
resolve()
}).then(()=>{
console.log(4)
})
});
// 第一阶段 Promise主体同步任务 输出5 目前结果1、5 ,then微任务 等待
// 第二阶段 then微任务 输出6 目前结果 1、5、6
new Promise((resovle)=>{
console.log(5)
resovle()
}).then(()=>{
console.log(6)
})
// 第一阶段 宏任务 等待 第一阶段处理同步任务完成 开始处理微任务
// 第二阶段 继续等待 外层同步任务和微任务处理完毕 开始处理宏任务
// 第三阶段 处理定时器宏任务 步骤和第一个定时器相同 按顺序执行输出7、8、9
setTimeout(() => {
console.log(7)
new Promise((resolve)=>{
console.log(8)
resolve()
}).then(()=>{
console.log(9)
})
});
// 最终结果1 5 6 2 3 4 7 8 9
this指向
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
  // 我们记住一句万能不变的话,谁调用this,this就指向谁

var a = 1
function func1(){
var a = 2
console.log(this.a+a)
}
function func2(){
var a = 10
func1()
}
func2()
// func2在全局调用,所以func2的this指向全局。于此同时func1被func2调用,所以func1的this指向func2,func2指向全局了拿到a=1,所以func1里面的this拿到的也是1。a则是自身内部的变量,最终结果就是1+2=3

var b = 1
var obj = {
b:2,
func1:function(){
console.log(this.b)
},
func2:()=>{
console.log(this.b)
}
}
obj.func1()
obj.func2()
// 答案是 2 1
// 了解一件事,一个函数如果被obj里的属性调用(对象.属性()形式),this指向这个对象本身
// func1函数,obj.func1()里打印this.b,此处的this指向obj对象,拿到的是obj的b
// func2箭头函数指向永远是外层的this,也就是说它指向obj的this,obj的this指向全局,所以这个箭头函数也间接指向全局,所以是1


// 看着都头大,慢慢分析,一个个来
const obj1 = {
name:"小明",
say(){
console.log(`你好!${this.name}`)
}
}
const func1 = obj1.say

obj1.say() // obj1.say是属性调用,this指向obj对象,所以获取到name,输出”你好!小明“
func1() // func1是通过obj1.say赋值的,func1在全局进行调用,谁调用this就指向谁,obj.say的this就指向全局,全局没有name,所以输出”你好!“

// 《javascript高级程序设计》(犀牛书)中提到,超时调用的代码都是在全局作用域中执行的,因此函数中的this在非严格模式模式下指向window对象,严格模式下为undefined
setTimeout(obj1.say,100) // 这里setTimeout执行obj1.say,原因和func1一样,this指向全局,全局没有name,所以输出“你好!“
setTimeout(func1,200) // setTimeout中执行func1也是同理,输出”你好!“
setTimeout(function (){
obj1.say()
},300) // 加了一个function后,不再是setTimeout直接执行obj的方法,所以这里是obj在调用属性,那么就指向obj对象,输出“你好!小明”
setTimeout(() => {
obj1.say()
}, 400) // 箭头函数,但是考察的还是谁调用了say,发现还是obj在调用属性,那就指向obj对象,输出“你好!小明”
斐波那契数列
1
2
3
4
5
6
7
8
9
10
11
12
13
function FibonacciSeq(n){
if(n===0|n===1){
return n
}
return FibonacciSeq(n-1)+FibonacciSeq(n-2)
}

fn(1) // 1
fn(2) // 1
fn(3) // 2
fn(4) // 3
fn(5) // 5
fn(6) // 8
数组扁平化并去重从小到大排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var arr = [1,[2,3],[4,[5,6]],[7,[8,9[10,11]]]]
// 第一种
function flatten1(arr){
while(arr.some(item=>Array.isArray(item))){
arr=[].concat(...arr)
}
return arr
}
console.log(Array.from(new Set(flatten1(arr))).sort())

// 第二种
function flatten2(arr){
return arr.toString().split(',')
}
console.log(Array.from(new Set(flatten2(arr))).sort())

// 第三种
function flatten3(arr){
return arr.flat(Infinity)
// arr.flat([depth]) depth是传递数组的展开深度(默认不填,数值为1),如果展开多层就传infinity
}
console.log(Array.from(new Set(flatten3(arr))).sort())
排序算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
const arr = [5,1,12,34,35,353,26,92,321,230,605,33,25,61]
// 1 冒泡排序
function bubbleSort(arr){
for(let i = 0;i<arr.length;i++){
for (let j = i+1;j<arr.length;j++){
if(arr[j]<arr[i]){
const temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
}
}
return arr
}
// 2 选择排序
function selectSort(arr){
for(let i = 0;i<arr.length;i++){
let index = i
for (let j = i+1;j<arr.length;j++){
// 这里找值比它小的下标并赋值给index
if(arr[index]>arr[j])
index=j
}
// 下标不等于本身说明新的index是更小的,就需要交换位置
if(index !== i){
const temp = arr[i]
arr[i] = arr[index]
arr[index] = temp
}
}
return arr
}
// 3 快速排序
function quickSort(arr){
// 如果数组长度就1不需要排序
if(arr.length <= 1){
return arr
}
// 找到数组中间的下标
let pivotIndex = Math.floor(arr.length/2)
// 把中间的那个数字拿出来作为基准值
let pivot = arr.splice(pivotIndex, 1)[0]
// 快排依据中间的数字把两边分开所以有left和right
let left = []
let right = []
// 进行比较,如果数字小于基准值,就加入左边的数组,否则加入右边
for(let i = 0;i<arr.length;i++){
if(arr[i]<pivot){
left.push(arr[i])
}else{
right.push(arr[i])
}
}
// 通过递归调用,不断定义新的基准值,不断进行放置,然后通过扩展运算符合并左、基准值、右即可
return [...quickSort(left),pivot,...quickSort(right),]
}
// 4 插入排序
function insertSort(arr){
let len = arr.length
for(let i = 1;i<len;i++){
let temp = arr[i]
let j = i
for(;j>0;j--){
if(temp >= arr[j-1]){
break
}
arr[j] = arr[j-1]
}
arr[j] = temp
}
return arr
}
手写call
1
2
3
4
5
6
7
8
9
10
11
Function.prototype.myCall = function(obj, ...rest){
obj = obj ? Object(obj):window
const fn = Symbol('test')
// 借助谁调用this,this就指向谁的特性
obj[fn] = this
// 把值给这个对象
const res = obj[fn](...rest)
// 把这个只是用来调this指向的test工具人删掉
delete obj[fn]
return res
}
手写apply
1
2
3
4
5
6
7
8
9
// 其实和call一样,apply只是传递参数不是直接传一堆,而是通过数组传一堆
Function.prototype.myApply = function(obj, arg=[]){
obj = obj ? Object(obj):window
const fn = Symbol('test')
obj[fn] = this
const res = obj[fn](...arg)
delete obj[fn]
return res
}
手写bind
1
2
3
4
5
6
7
// 其实和call一样,apply只是传递参数不是直接传一堆,而是通过数组传一堆
Function.prototype.myBind = function(obj,...rest1){
const fn = this
return function(...rest2){
return fn.myCall(obj,...rest1.concat(rest2))
}
}
判断回文
1
2
3
4
// 思路 直接判断翻转后的字符串数组是否等于原数组
function checkPalindrom(str) {
return str == str.split('').reverse().join('')
}
检查字符串中出现最多的元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function findMaxDuplicateChar(str) {
if (str.length == 1) {
return str
}
let charObj = {}
for (let i = 0; i < str.length; i++) {
if (!charObj[str.charAt(i)]) {
charObj[str.charAt(i)] = 1
} else {
charObj[str.charAt(i)] += 1
}
}
let maxChar = '', maxValue = 1
for (var k in charObj) {
if (charObj[k] >= maxValue) {
maxChar = k
maxValue = charObj[k]
}
}
console.log("出现的最大次数", maxValue)
// 出现次数最多的元素
return maxChar
}
求数组中元素的最大差值
1
2
3
4
5
6
7
8
9
10
11
function getMaxProfit(arr) {
var minPrice = arr[0]
var maxProfit = 0
for (var i = 0; i < arr.length; i++) {
var currentPrice = arr[i]
minPrice = Math.min(minPrice, currentPrice)
var potentialProfit = currentPrice - minPrice
maxProfit = Math.max(maxProfit, potentialProfit)
}
return maxProfit
}
不用中间变量交换(a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 方案A:利用加法
a = a + b
b = a - b
a = a - b

// 方案B:利用乘积
a = a * b
b = a / b
a = a / b

// 方案C:利用数组
a = [a, b]
b = a[0]
a = a[1]

// 方案D:利用与或
a = a ^ b
b = a ^ b
a = a ^ b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var Test = {
foo: "test",
func: function () {
var self = this
// func被Test调用,this指向test本身
// 因此this.foo为"test" 同理声明了self=this,self.foo也是"test"
// 那么重点来看自执行函数里的数据
// 自执行函数内部this指向全局对象,this指向外面,外面没有foo,因此为undefined
// 那么为什么self被赋值了this却有数据呢?因为这里它被创建在外层函数作用域里,并将赋值给当前的this。所以这里无论self塞到哪去,它的this都是指向外层函数(这里的外层是func)的this,所以它是能获取到foo值的
console.log(this.foo)
console.log(self.foo)
(function () {
console.log(this.foo)
console.log(self.foo)
})()
}
}
// 求打印结果
Test.func() // test test undefined test
代码返回true
1
2
3
4
[1,2,3,4,5].map(Number)	      // [1,2,3,4,5]
[1,2,3,4,5].forEach(Number) // undefined
[1,2,3,4,5].some(Number) // true
[1,2,3,4,5].every(Number) // true
无重复最长子串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var lengthOfLongestSubstring = function (s) {

// 创建一个长度和一个新的字符串
let len = 0 // 新长度索引 用来记录新子串长度
let str = '' // 新子串 用来记录连续的新子串

// 匹配字符 存在就拼接 不存在则剔除 从后一段截取
for (let i = 0; i < s.length; i++) {

// 判断 新子串 是否含有遍历元素
// -1 为不存在
if (str.indexOf(s[i]) === -1) {
str = str + s[i]
if (str.length > len) {
len = str.length
}
} else {
str = str + s[i];
let index = str.indexOf(s[i])
str = str.slice(index + 1)
}
}
return len
}
两数之和(leetcode简单题)
1
2
3
4
5
6
7
8
9
function twoSum(nums,target){
for (let i=0 ; i < nums.length; i++){
for (let j = i+1; j < nums.length; j++){
if(nums[i]+nums[j] === target){
return [i,j]
}
}
}
}
  • 标题: 前端常见面试题
  • 作者: Xxd
  • 创建于 : 2023-03-10 13:44:47
  • 更新于 : 2024-02-07 03:57:05
  • 链接: https://blog.xxdoge.site/2023/03/10/前端面试题/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
 评论
此页目录
前端常见面试题