HTML篇

1.src和href的区别

  • src属性在HTML标签中用于加载外部资源,其行为和效果取决于所使用的标签类型。通常情况下,它被用于加载图片、脚本、框架、音频和视频等外部资源。
  • href属性在HTML标签中用于创建超链接或引入外部样式表,其行为和效果取决于所使用的标签类型。通常情况下,它被用于创建超链接或引入外部样式表,以实现页面间的导航或样式的引入。

区别:

  1. 适用标签不同
    • src属性通常用于<img><script><iframe><audio><video>等标签,用于嵌入外部资源。
    • href属性通常用于<a><link><base>等标签,用于创建超链接或引入外部样式表。
  2. 加载行为不同
    • 对于src属性,浏览器会立即加载指定的资源,它会阻塞页面的加载,直到资源加载完成。
    • 对于href属性,它指定的资源通常在用户与页面交互时加载,不会阻塞页面的加载。
  3. 替换内容不同
    • src属性会替换元素原本的内容,例如,一个<img>标签的src属性指向的图片地址会显示在页面上,而原本<img>标签中的内容将被替换掉。
    • href属性不会替换元素原本的内容,例如,一个<a>标签的href属性指向的超链接地址会成为链接的目标,但不会影响<a>标签原本的内容。

2.Html5新增特性

语义化标签

  • 代码结构清晰,易于阅读,
  • 利于开发和维护 方便其他设备解析(如屏幕阅读器)根据语义渲染网页。
  • 有利于搜索引擎优化(SEO),搜索引擎爬虫会根据不同的标签来赋予不同的权重

增强表单属性

emailnumber时间控件color颜色拾取器placeholderautofocus自动获取焦点…

新增音视频标签

1
2
3
4
5
6
<audio controls>
<source src="horse.ogg" type="audio/ogg">
<source src="horse.mp3" type="audio/mpeg">
您的浏览器不支持 audio 元素。
</audio>

画布canvas

<canvas>HTML5 新增的,一个可以使用脚本(通常为 JavaScript) 在其中绘制图像的 HTML 元素。它可以用来制作照片集或者制作简单(也不是那么简单)的动画,甚至可以进行实时视频处理和渲染。

SVG绘图

  • SVG指可伸缩矢量图形
  • SVG用于定义用于网络的基于矢量的图形
  • SVG使用XML格式定义图形
  • SVG图像在放大或改变尺寸的情况下其图形质量不会有损失

SVG与Canvas的区别:

  • SVG适用于描述XML中的2D图形的语言
  • Canvas随时随地绘制2D图形(使用javaScript)
  • SVG是基于XML的,意味这可以操作DOM,渲染速度较慢
  • 在SVG中每个形状都被当做是一个对象,如果SVG发生改变,页面就会发生重绘
  • Canvas是一像素一像素地渲染,如果改变某一个位置,整个画布会重绘。

拖拉拽API

拖放是一种常见的特性,即捉取对象后拖到另一个位置

1
<div draggable="true"></div>

当元素拖动时,我们可以检查其拖动的数据。

WebStorage

使用HTML5可以在本地存储用户的浏览数据。早些时候,本地存储使用的是cookies。但是Web 存储需要更加的安全与快速. 这些数据不会被保存在服务器上,但是这些数据只用于用户请求网站数据上.它也可以存储大量的数据,而不影响网站的性能。数据以 键/值 对存在, web网页的数据只允许该网页访问使用。

websorage拥有5M的存储容量,而cookie却只有4K,这是完全不能比的。

客户端存储数据的两个对象为:

  • localStorage - 没有时间限制的数据存储
  • sessionStorage - 针对一个 session 的数据存储, 当用户关闭浏览器窗口后,数据会被删除。

API(以localStorage为例):

  • 保存数据:localStorage.setItem(key,value);
  • 读取数据:localStorage.getItem(key);
  • 删除单个数据:localStorage.removeItem(key);
  • 删除所有数据:localStorage.clear();
  • 得到某个索引的key:localStorage.key(index);

WebSocket

WebSocket协议为web应用程序客户端和服务端之间提供了一种全双工通信机制。

3.DOCTYPE(⽂档类型) 的作⽤

DOCTYPE是HTML5中一种标准通用标记语言的文档类型声明,是用来告诉浏览器的解析器,该用什么样的方式去加载识别文档。

一般是html和xml的方式

4.iframe 有那些优点和缺点?

iframe通常用来加载外部链接,不会影响网页内容的加载。

优点

  • 可以将网页原封不动的加载进来,用来加载显示较慢的内容,如广告、视频等

  • 增加代码的可用性

缺点

  • 加载的内容无法被浏览器引擎识别,对SEO不友好
  • 会阻塞onload事件加载

5.script标签中defer和async的区别

  • 常规的script标签会阻碍浏览器的解析,直到脚本加载并执行完毕
  • 带有async属性的script标签会异步加载脚本,加载完成后会立即执行,可能阻塞文档解析,也可能不阻塞,执行时机不确定
  • 带有defer属性的script标签也是异步加载脚本,但会在文档解析完成后执行,并且不会破坏脚本之间的依赖关系

6.块级元素、行内元素、行内块元素区别

块级元素有以下特点:

  • 每个块级元素都是独自占一行;
  • 高度,行高,外边距(margin)以及内边距(padding)都可以控制;
  • 元素的宽度如果不设置的话,默认为父元素的宽度(父元素宽度100%;
  • 多个块状元素标签写在一起,默认排列方式为从上至下;

行内元素有以下特点:

  • 不会独占一行,相邻的行内元素会排列在同一行里,直到一行排不下才会自动换行,其宽度随元素的内容而变化;
  • 高宽无效,对外边距(margin)和内边距(padding)仅设置左右方向有效 上下无效;
  • 设置行高有效,等同于给父级元素设置行高;
  • 元素的宽度就是它包含的文字或图片的宽度,不可改变;
  • 行内元素中不能放块级元素,a 链接里面不能再放链接;

行内块级元素具体特点如下:

行内块级元素,它既具有块级元素的特点,也有行内元素的特点,它可以自由设置元素宽度和高度,也可以在一行中放置多个行内块级元素。

元素类型转换 display

display:block ,定义元素为块级元素

display : inline ,定义元素为行内元素

display:inline-block,定义元素为行内块级元素

7.怎样添加、移除、移动、复制、创建和查找节点

添加节点document.appendchild(dom)

移除节点document.removechild(dom)

移动节点document.appendchild(targetDom)

复制节点dom.cloneNode(true),参数true表示是否复制子节点

创建节点document.createElement(dom)

查找节点

1
2
3
4
5
document.getElementById("elementId")
document.getElementsByClassName("className")
document.getElementsByTagName("tagName")
document.querySelector("selector")
document.querySelectorAll("selector")

CSS篇

1.设置背景透明

在 CSS 中设置背景透明的方式有多种,以下是其中几种常见的方式:

  1. 使用 RGBA 颜色值
    使用 RGBA 颜色值是一种常见的设置背景透明的方式。RGBA 表示红、绿、蓝和透明度(Alpha),透明度值介于 0(完全透明)和 1(完全不透明)之间。

    1
    2
    3
    .element {
    background-color: rgba(255, 255, 255, 0.5); /* 白色背景,透明度为 50% */
    }
  2. 使用透明 PNG 图片
    使用透明的 PNG 图片作为背景,可以实现背景的部分透明效果。

    1
    2
    3
    .element {
    background-image: url('transparent-background.png');
    }
  3. 使用 transparent 关键字
    transparent 关键字表示完全透明的颜色,可以直接将其作为背景颜色来实现背景透明。

    1
    2
    3
    .element {
    background-color: transparent; /* 完全透明的背景 */
    }
  4. 使用 CSS3 的 opacity 属性
    opacity 属性可以设置元素的透明度,包括元素的背景和内容。

    1
    2
    3
    .element {
    opacity: 0.5; /* 元素透明度为 50% */
    }

以上是一些常见的设置背景透明的方式,具体应根据实际需求选择适合的方法。

2.CSS3新增特性

新增CSS选择器、伪类

组合选择器

多元素选择器,为多个元素应用同一个样式;

1
2
3
4
h1, h2 {
background: yellow;
}

后代选择器,使一些样式、规则只在某一些指定的有“祖孙—后代关系”的后代元素上适用,其他非指定的结构中不适用;

1
2
3
4
5
ul em {
text-decoration: line-through;
background: yellow;
}

子元素选择器,使一些样式、规则只在某一些指定的有直接的“父子关系”的子元素上适用,其他非指定的结构中不适用;

1
2
3
4
5
p > em {
text-decoration: line-through;
background: yellow;
}

直接相邻元素选择器,前提,两个元素有共同的父元素,且后一个元素“紧接”在前一个元素后边时,你想对后一个元素添加样式;

1
2
3
4
5
h2 + p {
text-decoration: line-through;
background: yellow;
}

普通相邻元素选择器,相对于“直接相邻元素选择器”而言,两个元素也必须有共同的父元素,但后一个元素不需要“紧接”在前一个元素后边,你也可以对后一个元素添加样式;

1
2
3
4
5
h2 ~ h2 {
text-decoration: line-through;
background: yellow;
}

特效:text-shadowbox-shadow

线性渐变: gradient

旋转过渡:transformtranstion

动画: animation

圆角: border-radius

3.伪元素和伪类的区别?

伪元素(pseudo-elements)和伪类(pseudo-classes)是 CSS 中用于选择元素的特殊方式,它们在语法和作用上有一些区别。

  1. 伪元素(pseudo-elements)

    • 伪元素用于向选中的元素添加特殊的样式,以创建一些不在文档树中的额外元素。这些额外元素通常用于装饰或添加内容,但不会真正改变文档内容。
    • 伪元素以双冒号 :: 开头,例如 ::before::after
    • 常见的伪元素包括 ::before::after::first-line::first-letter 等。
    • 伪元素在 CSS2 中以单冒号 : 开头,但在 CSS3 中规范中改为使用双冒号 ::
  2. 伪类(pseudo-classes)

    • 伪类用于选择文档中的某些状态或特定的元素,它们并不创建新的元素,而是根据元素的状态或位置来进行选择。
    • 伪类以单冒号 : 开头,例如 :hover:nth-child()
    • 常见的伪类包括 :hover:active:focus:first-child:nth-child() 等。

总的来说,伪元素用于创建额外的元素或样式效果,而伪类用于选择元素的特定状态或位置。在实际使用中,伪元素通常用于添加额外的装饰或内容,而伪类用于响应用户的交互或选择特定位置的元素。

4.隐藏元素的方式

  • display:none:元素在文档中不存在,不会占据位置。
  • visibility: hidden:元素在文档中的位置还保留,仍然占据空间。
  • opacity:0:将透明度设置为0。
  • z-index:负值:直接将元素放置在最下层,利用其他元素来遮盖。
  • position:absolute:将元素定位到可视区域以外。

5.有了使用过Sass、Less 吗?他们的区别是什么?

他们都是 CSS 预处理器,是 CSS 上的一种抽象层。他们是一种特殊的语法/语言编译成 CSS。 增加了 CSS代码的复用性,层级,mixin, 变量,循环, 函数等对编写以及开发UI组件都极为方便。 区别

  1. 编译环境不一样
    • Sass是在服务端处理的,以前是Ruby,现在是Dart-SassNode-Sass
    • Less是需要引入less.js来处理Less代码输出CSS到浏览器,也可以在开发服务器将Less语法编译成css文件,输出CSS文件到生产包目录
  2. 变量符不一样,Less是@,而Scss是$
  3. Sass支持条件语句,可以使用if{}else{},for{}循环等等。而Less不支持

6.link和@import的区别

  • link是HTML提供的标签,不仅可以加载CSS文件,还可以定义RSS、rel连接属性等
  • @import是CSS提供等语法规则,只有导入样式表带作用。
  • link标签引入的CSS被同时加载,而@import引入的CSS将在页面加载完毕后被加载

7.常见的CSS单位

  • px像素
    • CSS像素
    • 物理像素
  • **百分比%**,作用于父元素, 当浏览器的宽度或者高度发生变化时,当前元素依据比例发生变化。
  • em和rem,相对长度单位,它们之间的区别:em相对于父元素,rem相对于根元素。
  • vw/vh是与视图窗口有关的单位,代表视图窗口的宽高。

px、em、rem的区别

  • px 固定像素单位,不能随其它元素的变化而变化
  • em是相对于父元素的单位,会随着父元素变化而变化
  • rem是相对于根元素html,它会随着html元素变化而变化

8.两栏布局

  • 利用浮动,将左边元素宽度设置为200px,并且设置向左浮动。将右边元素的margin-left设置为200px,宽度设置为auto(默认为auto,撑满整个父元素)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.outer {
height: 1000px;
}
.left {
float: left;
width: 200px;
height: 1000px;
background: tomato;
}
.right {
margin-left: 200px;
height: 1000px;
width: auto;
background: gold;
}
  • 利用浮动,左侧元素设置固定大小,并左浮动,右侧元素设置overflow: hidden; 这样右边就触发了BFC,BFC的区域不会与浮动元素发生重叠,所以两侧就不会发生重叠。
1
2
3
4
5
6
7
8
9
10
11
12
.left{
width: 100px;
height: 200px;
background: red;
float: left;
}
.right{
height: 300px;
background: blue;
overflow: hidden;
}

  • 利用flex布局,将左边元素设置为固定宽度200px,将右边的元素设置为flex:1。
1
2
3
4
5
6
7
8
9
10
11
12
13
.outer {
display: flex;
height: 100px;
}
.left {
width: 200px;
background: tomato;
}
.right {
flex: 1;
background: gold;
}

  • 利用绝对定位,将父级元素设置为相对定位。左边元素设置为absolute定位,并且宽度设置为200px。将右边元素的margin-left的值设置为200px。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.outer {
position: relative;
height: 100px;
}
.left {
position: absolute;
width: 200px;
height: 100px;
background: tomato;
}
.right {
margin-left: 200px;
background: gold;
}

9.水平垂直居中

  • 利用绝对定位,先将元素的左上角通过top:50%left:50%定位到页面的中心,然后再通过translate来调整元素的中心点到页面的中心。该方法需要考虑浏览器兼容问题。
1
2
3
4
5
6
7
8
9
10
11
.parent {
position: relative;
}

.child {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}

  • 利用绝对定位,设置四个方向的值都为0,并将margin设置为auto,由于宽高固定,因此对应方向实现平分,可以实现水平和垂直方向上的居中。该方法适用于盒子有宽高的情况:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    .parent {
    position: relative;
    }

    .child {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    margin: auto;
    }

  • 使用flex布局,通过align-items:centerjustify-content:center设置容器的垂直和水平方向上为居中对齐,然后它的子元素也可以实现垂直和水平的居中。该方法要考虑兼容的问题,该方法在移动端用的较多:

    1
    2
    3
    4
    5
    6
    .parent {
    display: flex;
    justify-content:center;
    align-items:center;
    }

10.flex布局理解

flex布局是CSS3新增的一种布局方式,能够根据不同屏幕尺寸的变化来自适应大小。

常用的属性:

  • flex-direction属性决定主轴的方向(即项目的排列方向)。
  • flex-wrap属性定义,如果一条轴线排不下,如何换行。
  • flex-flow属性是flex-direction属性和flex-wrap属性的简写形式,默认值为row nowrap
  • justify-content属性定义了项目在主轴上的对齐方式。
  • align-items属性定义项目在交叉轴上如何对齐。
  • align-content属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。

flex: 1表示什么

flex : 1 其实是代表3个属性的简写:flex-grow、flex-shirnk、flex-basis,这3个属性是可以单独进行设置的

当设置了 flex:1 属性时,代表 flex-grow: 1、flex-shirnk:1、flex-basis:0%

11.对BFC的理解,如何创建BFC

BFC是块级格式上下文(Block Formatting Context,BFC),是CSS布局的一个概念,在BFC布局里面的元素不受外面元素影响。

创建BFC条件

  • 设置浮动:float有值并不为空
  • 设置绝对定位: position(absolute、fixed)
  • overfilow值为:hiddenauto、`scroll
  • display值为:inline-blocktable-celltable-captionflex

BFC作用

  • 解决margin重叠问题:由于BFC是一个独立的区域,内部元素和外部元素互不影响,将两个元素变为BFC,就解决了margin重叠问题
  • 创建自适应两栏布局:可以用来创建自适应两栏布局,左边宽高固定,右边宽度自适应。
  • 解决高度塌陷问题:在子元素设置浮动后,父元素会发生高度的塌陷,也就是父元素的高度为0解决这个问题,只需要将父元素变成一个BFC。

12.什么是margin重叠,如何解决

两个块级元素分别设置上下margin时可能会导致边距合并为一个边距,合并到边距取最大的那个值。需要注意的是,浮动的元素和绝对定位这种脱离文档流的元素的外边距不会折叠。重叠只会出现在垂直方向。

计算规则

  • 都是正数,取最大的。20px 40px ---> 40px
  • 一正一负,用正数减去负数后。20px -50px ---> -30px
  • 都是负数,用0减去两个中绝对值大的那个。-30px -10px ---> -20px

解决方案

对于重叠的情况,主要有两种:兄弟之间重叠(margin合并)父子之间重叠(margin塌陷)

  • 兄弟之间重叠
    • 底部元素变为行内盒子:display: inline-block
    • 底部元素设置浮动:float
    • 底部元素的position的值为absolute/fixed
  • 父子之间重叠
    • 父元素加入:overflow: hidden
    • 父元素添加透明边框:border:1px solid transparent
    • 子元素变为行内盒子:display: inline-block
    • 子元素加入浮动属性或定位

13.position 常用属性

  • static 默认值,没有定位,元素正常在文档流中显示
  • relative 相对定位,相对于原来的位置进行定位
  • absolute 绝对定位,相对于static定位意外以外的一个父元素进行定位。
  • fixed 绝对定位,相对于浏览器窗口
  • sticky 粘性定位,基于用户滚动位置

14.实现一个三角形

通过设置不同方向边框来实现

1
2
3
4
5
6
7
8
div {
width: 0;
height: 0;
border-top: 50px solid red;
border-right: 50px solid transparent;
border-left: 50px solid transparent;
}

15.画一条0.5px的线

使用 transform: scale() 方法绘制 0.5px 的线条,可以确保在大多数设备上准确渲染出细微的线条。这里是如何实现的示例:

水平线条
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>0.5px Line</title>
<style>
.horizontal-line {
width: 100%;
height: 1px; /* Initial height */
background-color: black;
transform: scaleY(0.5);
transform-origin: top;
}
</style>
</head>
<body>
<div class="horizontal-line"></div>
</body>
</html>
垂直线条
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>0.5px Line</title>
<style>
.vertical-line {
width: 1px; /* Initial width */
height: 100px;
background-color: black;
transform: scaleX(0.5);
transform-origin: left;
}
</style>
</head>
<body>
<div class="vertical-line"></div>
</body>
</html>
解释
  • 水平线条: 通过将一个高度为1px的div使用transform: scaleY(0.5)来缩放,使其显示为0.5px的高度。transform-origin: top确保了缩放是从上边缘开始。
  • 垂直线条: 通过将一个宽度为1px的div使用transform: scaleX(0.5)来缩放,使其显示为0.5px的宽度。transform-origin: left确保了缩放是从左边缘开始。
注意事项
  1. 初始尺寸: 确保初始尺寸设置为1px(例如,水平线条的高度或垂直线条的宽度),这样缩放后能正确显示为0.5px。
  2. 浏览器支持: transform属性在现代浏览器中有很好的支持,但对于非常旧的浏览器可能会有兼容性问题。

16.如何解决1px

在移动端的高DPI(如Retina)屏幕上,CSS中的1px可能显得比预期的粗,这是因为这些屏幕的设备像素比(device pixel ratio, DPR)通常大于1。为了解决这个问题,可以使用几种方法来确保线条在所有设备上都能呈现出一致的1px效果。

方法一:直接使用0.5px

对于现代浏览器,可以直接使用0.5px来绘制细线条。这种方法简单直接,但需要确保浏览器支持。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>0.5px Line</title>
<style>
.line {
width: 100%;
border-top: 0.5px solid black;
}
</style>
</head>
<body>
<div class="line"></div>
</body>
</html>
方法二:利用伪元素,先放大再缩小

使用伪元素创建一个1px的线条,通过transform: scale来缩小为0.5px。这样可以确保在高DPI屏幕上也能正确显示。

示例

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>1px Line</title>
<style>
.line {
position: relative;
}
.line::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 1px;
background-color: black;
transform: scaleY(0.5);
transform-origin: top;
}
</style>
</head>
<body>
<div class="line"></div>
</body>
</html>
方法三:使用viewport缩放

通过控制viewport的缩放比例来解决1px问题。虽然这种方法较少使用,但在特定情况下可能有效。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=0.5">
<title>Viewport Scale</title>
<style>
.line {
width: 100%;
border-top: 1px solid black;
}
</style>
</head>
<body>
<div class="line"></div>
</body>
</html>

这种方法将整个页面缩小50%,然后用CSS缩放回去。不过,这会影响整个页面的布局,因此需要慎重使用。

解释
  1. 直接写0.5px: 这是最直接的方法,现代浏览器大多支持。不过在一些旧浏览器上可能不生效。
  2. 利用伪元素: 通过创建伪元素,先绘制1px的线条,然后使用CSS的transform: scale进行缩小。这样可以确保线条在所有设备上都显示为0.5px。
  3. 使用viewport缩放: 通过调整viewport缩放比例,可以缩小页面元素的实际显示尺寸,然后再用CSS调整回来。这种方法影响范围较大,但在某些特定场景下有效。

通过这些方法,可以解决高DPI设备上1px线条显示过粗的问题,确保视觉效果的一致性。

JavaScript篇

1.什么是回调函数?

回调函数也就是 “回头再调用函数”,把函数指针(引用)通过参数的形式传递给某个函数,该函数在它自身调用之后再调用你传递的函数。

通常将一个函数B传入另一个函数A,并且在需要的时候再调用函数A。

对回调函数的理解就是,定义了一个函数,不去调用他,但是这个函数会在特定时间条件下被调用

回调函数常用于处理异步操作,如网络请求、计时器、事件监听等。

回调函数的用途
  1. 处理异步操作: 回调函数通常用于处理异步任务,如网络请求、数据库操作等。
  2. 事件处理: 在事件驱动的编程中,回调函数用于处理用户交互事件,如点击、鼠标移动等。
  3. 数组方法: 在数组的方法中如 map, filter, reduce 等,回调函数被用来处理数组的每一个元素。
注意事项
  • 回调地狱: 过度嵌套的回调函数会导致代码难以维护和理解,通常称为“回调地狱”或“回调金字塔”。
  • 错误处理: 在异步操作中,确保正确处理错误和异常。否则可能会导致未预料的行为。
现代替代方案

为了避免回调地狱和使异步代码更清晰,现代JavaScript引入了Promises和async/await语法。

2.什么是闭包?优缺点分别是什么?

定义

闭包(Closure)是指在 JavaScript 中,函数能够访问它自身作用域之外的变量

1
2
3
4
5
6
7
8
9
10
11
function f1() {
var n = 999;
function f2() {
alert(n); // 999
}
return f2;
}

var result = f1(); // 执行 f1,并返回 f2 函数
result(); // 调用 result,即 f2 函数,弹出警告框显示 999

工作原理
  1. 当我们调用 f1() 时,f1 函数执行并返回其内部的 f2 函数。
  2. 即使 f1 函数已经执行完毕,f2 函数仍然保有对 f1 作用域中 n 变量的引用。
  3. 当我们调用 result() 时,它实际上是调用 f2 函数,并且可以访问 f1 中定义的 n 变量。
优点:

1.防止变量污染作用域

2.闭包可以创建私有变量和方法

3.闭包可以用来记住函数执行时的状态信息

4.闭包在处理异步操作和回调函数时非常有用,可以在异步操作完成时保留并访问执行上下文

缺点:
1.内存消耗:

读取函数内部的变量让函数内部的变量的值始终保持在内存中,不会在父函数调用后被自动清除

解决方法:

1.手动释放引用

1
2
3
4
5
6
7
8
9
10
11
12
function createClosure() {
var largeObject = {}; // 假设这是一个大的对象
function innerFunction() {
console.log(largeObject);
}
return innerFunction;
}

var closure = createClosure();
// 当不再需要闭包时,释放对 largeObject 的引用
closure = null;

2.使用现代语言特性

利用 letconst 声明块级作用域变量,避免不必要的全局变量泄漏

2.性能问题

解决方法:

1.避免不必要的闭包

2.避免频繁创建和销毁闭包

3.避免频繁创建和销毁闭包

4.使用事件委托

3.js数据类型有哪些

JavaScript共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt

其中 SymbolBigInt 是ES6 中新增的数据类型:

  • Symbol代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
  • BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

这些数据可以分为原始数据类型引用数据类型(复杂数据类型),他们在内存中的存储方式不同。

  • 堆: 存放引用数据类型,引用数据类型占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址,如ObjectArrayFunction
  • 栈: 存放原始数据类型,栈中的简单数据段,占据空间小,属于被频繁使用的数据,如StringNumberNullBoolean

4.null和undefined区别

UndefinedNull 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefinednull

  • undefined 代表的含义是未定义,一般变量声明了但还没有定义的时候会返回 undefinedtypeofundefined
  • null 代表的含义是空对象,null主要用于赋值给一些可能会返回对象的变量,作为初始化,typeofobject
1
2
3
4
null == undefined // true

null === undefined //false

5.instanceof 运算符的实现原理

instanceof 运算符的原理是基于原型链的查找。当使用 obj instanceof Constructor 进行判断时,JavaScript 引擎会从 obj 的原型链上查找 Constructor.prototype 是否存在,如果存在则返回 true,否则继续在原型链上查找。如果查找到原型链的顶端仍然没有找到,则返回 false

instanceof运算符只能用于检查某个对象是否是某个构造函数的实例,不能用于基本类型的检查,如stringnumber

6.typeof 和 instanceof 区别

typeofinstanceof 都是判断数据类型的方法,区别如下:

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

这是 JavaScript 语言的一个历史遗留问题

7.为什么0.1+0.2 ! == 0.3,如何让其相等

原因

因为浮点数运算的精度问题。在计算机运行过程中,需要将数据转化成二进制,然后再进行计算。 因为浮点数自身小数位数的限制而截断的二进制在转化为十进制,就变成0.30000000000000004,所以在计算时会产生误差。

解决方案
  • 将其先转换成整数,再相加之后转回小数。具体做法为先乘10相加后除以10
1
2
let x=(0.1*10+0.2*10)/10;
console.log(x===0.3)
  • 使用number对象的toFixed方法,只保留一位小数点。
1
(n1 + n2).toFixed(2)

8.判断数组的方式有哪些

通过Object.prototype.toString.call()做判断

1
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';

通过原型链做判断

1
obj.__proto__ === Array.prototype;

通过ES6的Array.isArray()做判断

1
Array.isArrray(obj);

通过instanceof做判断

1
obj instanceof Array

9.对类数组对象的理解,如何转化为数组

类数组也叫伪数组,类数组和数组类似,但不能调用数组方法,常见的类数组有arguments、通过document.getElements获取到的内容等,这些类数组具有length属性

通过 Array.from 方法来实现转换

1
Array.from(arrayLike)

通过 call 调用数组的 slice 方法来实现转换

1
Array.prototype.slice.call(arrayLike)

10.数组有哪些方法

数组和字符串的转换方法:toString()toLocalString()join() 其中 join() 方法可以指定转换为字符串时的分隔符。

数组尾部操作的方法 pop() push()push 方法可以传入多个参数。

数组首部操作的方法 shift()unshift() 重排序的方法 reverse() sort()sort() 方法可以传入一个函数来进行比较,传入前后两个值,如果返回值为正数,则交换两个参数的位置。

数组连接的方法 concat() ,返回的是拼接好的数组,不影响原数组。

数组截取办法 slice(),用于截取数组中的一部分返回,不影响原数组。

数组插入方法 splice(),影响原数组查找特定项的索引的方法,indexOf()lastIndexOf() 迭代方法 every()some()filter()map()forEach()方法

数组归并方法 reduce() reduceRight() 方法

改变原数组的方法fill()pop()push()shift()splice()unshift()reverse()sort()

不改变原数组的方法concat()every()filter()find()findIndex()forEach()indexOf()join()lastIndexOf()map()reduce()reduceRight()slice()some()

11.substring和substr的区别

它们都是字符串方法,用于截取字符串的一部分,主要区别在于参数不同

  • substring(startIndex, endIndex): 接收两个参数,一个起始索引和结束索引,来指定字符串范围,如果省略第二个参数,则截取到字符串末尾。
  • substr(startIndex, length): 接收两个参数,并返回从 startIndex 开始,长度为 length 的子字符串。如果省略第二个参数,则截取到字符串末尾。
1
2
3
4
5
6
const str = "Hello, World!";

console.log(str.substring(0, 5)); // 输出: "Hello"

console.log(str.substr(7, 5)); // 输出: "World"

12.知道object.assign吗

Object.assign() 是 JavaScript 中一个用于将一个或多个源对象的可枚举属性复制到目标对象的静态方法。它返回目标对象。该方法常用于对象的浅拷贝和合并。

语法
1
Object.assign(target, ...sources)
  • target: 目标对象,将要接收源对象的属性。
  • sources: 一个或多个源对象,这些对象的属性将被复制到目标对象。
基本用法

1. 对象的浅拷贝

Object.assign() 可以用于对象的浅拷贝(即仅复制对象的顶层属性)。

1
2
3
4
const obj = { a: 1, b: 2 };
const copy = Object.assign({}, obj);

console.log(copy); // { a: 1, b: 2 }

注意:这是浅拷贝,因此嵌套对象(或数组)仍然是共享引用。

2. 对象的合并

Object.assign() 可以合并多个对象,将多个源对象的属性复制到目标对象。

1
2
3
4
5
6
7
const o1 = { a: 1 };
const o2 = { b: 2 };
const o3 = { c: 3 };

const mergedObject = Object.assign({}, o1, o2, o3);

console.log(mergedObject); // { a: 1, b: 2, c: 3 }

如果多个源对象有相同的属性,则后面的属性将覆盖前面的属性。

1
2
3
4
5
6
const o1 = { a: 1, b: 2 };
const o2 = { b: 3, c: 4 };

const mergedObject = Object.assign({}, o1, o2);

console.log(mergedObject); // { a: 1, b: 3, c: 4 }

3. 继承属性和不可枚举属性

Object.assign() 只会复制源对象的自身可枚举属性(不包括继承的属性和不可枚举的属性)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const obj = Object.create({ foo: 1 }, {
bar: {
value: 2,
enumerable: true
},
baz: {
value: 3,
enumerable: false
}
});

const copy = Object.assign({}, obj);

console.log(copy); // { bar: 2 }

4. 修改目标对象

Object.assign() 会直接修改目标对象本身,并且返回目标对象。如果目标对象需要是一个全新的对象,通常会用空对象 {} 作为目标对象。

1
2
3
4
5
6
7
8
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const returnedTarget = Object.assign(target, source);

console.log(target); // { a: 1, b: 4, c: 5 }
console.log(returnedTarget); // { a: 1, b: 4, c: 5 }
console.log(target === returnedTarget); // true
注意事项
  • 浅拷贝: Object.assign() 执行的是浅拷贝,而不是深拷贝。如果源对象的某个属性是对象或数组,目标对象获得的只是这个对象或数组的引用。

    1
    2
    3
    4
    5
    const obj1 = { a: { b: 1 } };
    const obj2 = Object.assign({}, obj1);

    obj1.a.b = 2;
    console.log(obj2.a.b); // 2
  • 异常会中断复制过程: 如果在复制属性的过程中抛出异常,那么 Object.assign() 方法将不会处理剩余的源对象,并且目标对象已经添加的属性也不会被回滚。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const target = {};
    const source = {
    get a() {
    throw new Error('This will stop the copying process');
    },
    b: 2
    };

    try {
    Object.assign(target, source);
    } catch (e) {
    console.error(e.message); // "This will stop the copying process"
    }

    console.log(target); // {}
总结

Object.assign() 是一个强大的工具,适用于对象的浅拷贝和合并。但由于它执行的是浅拷贝,因此在处理嵌套对象时需要特别注意。如果需要进行深拷贝,可能需要使用其他方法,如递归复制对象或使用第三方库(如 Lodash 的 _.cloneDeep)。

13.知道扩展操作符吗

扩展操作符(spread operator,...)在对象或数组拷贝时执行的是浅拷贝。浅拷贝仅复制对象或数组的顶层属性,嵌套对象或数组则依然是共享同一引用。

对象的浅拷贝

扩展操作符可以用于对象的浅拷贝:

1
2
3
4
5
6
7
8
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { ...obj1 };

console.log(obj2); // { a: 1, b: { c: 2 } }

// 修改 obj1 中的嵌套对象属性
obj1.b.c = 3;
console.log(obj2.b.c); // 3

在这个例子中,obj2obj1 的浅拷贝,因此修改 obj1 中嵌套对象 b 的属性 c 会影响 obj2

数组的浅拷贝

扩展操作符同样可以用于数组的浅拷贝:

1
2
3
4
5
6
7
8
const arr1 = [1, [2, 3]];
const arr2 = [...arr1];

console.log(arr2); // [1, [2, 3]]

// 修改 arr1 中的嵌套数组
arr1[1][0] = 4;
console.log(arr2[1][0]); // 4

在这个例子中,arr2arr1 的浅拷贝,因此修改 arr1 中嵌套数组的内容会影响 arr2

14.new操作符的实现原理

new操作符的执行过程:

  1. 创建一个空对象
  2. 设置原型,将构造函数的原型指向空对象的 prototype 属性。
  3. this 指向这个对象,通过apply执行构造函数。
  4. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象

15.for…in和for…of的区别

for...in 循环主要是为了遍历对象而生,不适用于遍历数组;for...of 循环可以用来遍历数组、类数组对象,字符串、SetMap 以及 Generator 对象。

16.对AJAX的理解

AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。 创建AJAX请求的步骤:

  • 创建一个 XMLHttpRequest 对象。
  • 在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
  • 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。
  • 当对象的属性和监听函数设置完成后,最后调用 send 方法来向服务器发起请求,可以传入参数作为发送的数据体。

17.实现一个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
25
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", url, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功时
if (this.status === 200) {
handle(this.response);
} else {
console.error(this.statusText);
}
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);


18.ajax、axios、fetch的区别

1.ajax

英译过来是Aysnchronous JavaScript And XML,直译是异步JSXMLXML类似HTML,但是设计宗旨就为了传输数据,现已被JSON代替),解释一下就是说XML作为数据传输格式发送JS异步请求。但实际上ajax是一个一类技术的统称的术语,包括XMLHttpRequestJSCSSDOM等,它主要实现网页拿到请求数据后不用刷新整个页面也能呈现最新的数据

下面我们简单封装一个ajax请求【面试高频题】:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
javascript复制代码const ajaxGet = function (url) {
const xhr = new XMLHttpRequest()
xhr.open('get', url)
xhr.onreadystatechange = () => {
if (xhr.readyState == 4) {
if (xhr.status >= 200 && xhr.status < 400) {
console.log(xhr.response); // 响应结果
}
}
}
xhr.onerror = (error) => {
console.log(error, xhr.status)
}
xhr.send()
}
2. fetch

它其实就是一个JS自带的发送请求的一个api,拿来跟ajax对比是完全不合理的,它们完全不是一个概念的东西,适合拿来和fetch对比的其实是xhr,也就是上面封装ajax请求的代码里的XMLHttpRequest,这两都是JS自带的发请求的方法,而fetchES6出现的,自然功能比xhr更强,主要原因就是它是基于Promise的,它返回一个Promise,因此可以使用.then(res => )的方式链式处理请求结果,这不仅提高了代码的可读性,还避免了回调地狱(xhr通过xhr.onreadystatechange= () => {}这样回调的方式监控请求状态,要是想在请求后再发送请求就要在回调函数内再发送请求,这样容易出现回调地狱)的问题。而且JS自带,语法也非常简洁,几行代码就能发起一个请求,用起来很方便,据说大佬都爱用。

它的特点是:

  • 使用 promise,不使用回调函数。
  • 采用模块化设计,比如 rep、res 等对象分散开来,比较友好。
  • 通过数据流对象处理数据,可以提高网站性能。

下面我们简单写个fetch请求的示例:

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
javascript复制代码// get请求
fetch('http://127.0.0.1:8000/get')
.then(res => {
if (!res.ok) {
throw new Error('请求错误!状态码为:', res.status)
}
return res.text()
}).then(data => {
console.log(data);
})
// post请求
fetch('http://127.0.0.1:8000/post', {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
mode: 'no-cors', // 设置cors表示只能发送跨域的请求,no-cors表示跨不跨域都能发
body: JSON.stringify({
name: 'zhangsan',
age: 18
})
}).then(res => {
return res.json()
}).then(data => {
console.log(data);
})
3. axios

axios是用于网络请求的第三方库,它是一个库。axios利用xhr进行了二次封装的请求库,xhr只是axios中的其中一个请求适配器,axios在nodejs端还有个http的请求适配器;axios = xhr + http;它返回一个Promise。【项目中经常需要封装的axios】

它的特点:

  • 在浏览器环境中创建 XMLHttpRequests;在node.js环境创建 http 请求
  • 返回Promise
  • 拦截请求和响应
  • 自动转换 JSON 数据
  • 转换请求数据和响应数据
  • 取消请求

它的基础语法是:

1
2
3
4
5
6
7
8
9
10
11
12
// 发送 Get 请求
axios({
method: 'get',
url: '',
params: {} // 查询query使用params
})
// 发送 Post 请求
axios({
method: 'post',
url: '',
data: {} // 请求体body用data
})

下面我们在vue项目中封装一个使用axios实现的请求。

libs/config.js:配置文件

1
2
3
4
5
const serverConfig = {
baseUrl: "http://127.0.0.1:8000", // 请求基础地址,可根据环境自定义
useTokenAuthentication: false, // 是否开启token认证
};
export default serverConfig;

libs/request.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
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
79
80
81
82
83
84
85
86
87
88
89
90
91
import axios from "axios";  // 第三方库 需要安装
import serverConfig from "./config";
// 创建axios实例
const apiClient = axios.create({
baseURL: serverConfig.baseUrl, // 基础请求地址
withCredentials: false, // 跨域请求是否需要携带cookie
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
timeout: 10000, // 请求超时时间
});

// 请求拦截
apiClient.interceptors.request.use(
(config) => {
// 请求发送前的处理逻辑 比如token认证,设置各种请求头啥的
// 如果开启token认证
if (serverConfig.useTokenAuthentication) {
// 请求头携带token
config.headers.Authorization = localStorage.getItem("token");
}
return config;
},
(error) => {
// 请求发送失败的处理逻辑
return Promise.reject(error);
}
);

// 响应拦截
apiClient.interceptors.response.use(
(response) => {
// 响应数据处理逻辑,比如判断token是否过期等等
// 代码块
return response;
},
(error) => {
// 响应数据失败的处理逻辑
let message = "";
if (error && error.response) {
switch (error.response.status) {
case 302:
message = "接口重定向了!";
break;
case 400:
message = "参数不正确!";
break;
case 401:
message = "您未登录,或者登录已经超时,请先登录!";
break;
case 403:
message = "您没有权限操作!";
break;
case 404:
message = `请求地址出错: ${error.response.config.url}`;
break;
case 408:
message = "请求超时!";
break;
case 409:
message = "系统已存在相同数据!";
break;
case 500:
message = "服务器内部错误!";
break;
case 501:
message = "服务未实现!";
break;
case 502:
message = "网关错误!";
break;
case 503:
message = "服务不可用!";
break;
case 504:
message = "服务暂时无法访问,请稍后再试!";
break;
case 505:
message = "HTTP 版本不受支持!";
break;
default:
message = "异常问题,请联系管理员!";
break;
}
}
return Promise.reject(message);
}
);

export default apiClient;

/api/index.js:配置请求接口,这里一个get一个post

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import apiClient from "@/libs/request";

let getInfo = (params) => {
return apiClient({
url: "/get",
method: "get",
params, // axios的get请求query用params
});
};
let postInfo = (params) => {
return apiClient({
url: "/post",
method: "post",
data: params, // axios的post请求body用data
});
};
export default {
getInfo,
postInfo,
};

App.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
<script>
import api from './api/index.js'
export default {
data() {
return {
isH5: true
}
},
created() {
this.init()
},
methods: {
init() {
api.getInfo().then(res => {
console.log(res.data);
})
api.postInfo({
name: 'zhangsan',
age: '18'
}).then(res => {
console.log(res.data);
})
}
},
}
</script>

19.forEach和map方法有什么区别

两个方法都是用来遍历循环数组,区别如下:

  • forEach()对数据的操作会改变原数组,该方法没有返回值;
  • map()方法不会改变原数组的值,返回一个新数组,新数组中的值为原数组调用函数处理之后的值;

20.什么是尾调用,使用尾调用有什么好处?

尾调用就是在函数的最后一步调用函数,在一个函数里调用另外一个函数会保留当前执行的上下文,如果在函数尾部调用,因为已经是函数最后一步,所以这时可以不用保留当前的执行上下文,从而节省内存。但是ES6的尾调用只能在严格模式下开启,正常模式是无效的。

21.用过哪些设计模式

单例模式:保证类只有一个实例,并提供一个访问它的全局访问点。

工厂模式:用来创建对象,根据不同的参数返回不同的对象实例。

策略模式:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

装饰器模式:在不改变对象原型的基础上,对其进行包装扩展。

观察者模式:定义了对象间一种一对多关系,当目标对象状态发生改变时,所有依赖它对对象都会得到通知。

发布订阅模式: 基于一个主题/事件通道,希望接收通知的对象通过自定义事件订阅主题,被激活事件的对象(通过发布主题事件的方式被通知)。

22.如何实现深浅拷贝

深拷贝

1.JSON.stringify()将js对象序列化,再通过JSON.parse反序列

  • 如果对象中有函数、undefinedsymbol时,都会丢失
  • 如果有正则表达式、Error对象等,会得到空对象

2.递归方式

浅拷贝
  • Objec.assign() 拷贝对象
  • 扩展运算符

23.call() 、bind()、 apply() 的区别?

  • 都可以用作改变this指向
  • callapply的区别在于传参,callbind都是传入对象。apply传入一个数组。
  • callapply改变this指向后会立即执行函数,bind在改变this后返回一个函数,不会立即执行函数,需要手动调用。

image-20240517214224245

连续多个 bind,最后this指向是什么?

JavaScript 中,连续多次调用 bind 方法,最终函数的 this 上下文是由第一次调用 bind 方法的参数决定的

1
2
3
4
5
6
7
8
9
10
11
const obj1 = { name: 'obj1' };
const obj2 = { name: 'obj2' };
const obj3 = { name: 'obj3' };

function getName() {
console.log(this.name);
}

const fn1 = getName.bind(obj1).bind(obj2).bind(obj3);
fn1(); // 输出 "obj1"

24.理解原型

  • prototype : js通过构造函数来创建对象,每个构造函数内部都会一个原型prototype属性,它指向另外一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。
  • *proto*: 当使用构造函数创建一个实例对象后,可以通过__proto__访问到prototype属性。
  • constructor:实例对象通过这个属性可以访问到构造函数

25.理解原型链

每个实例对象都有一个__proto__属性指向它的构造函数的原型对象,而这个原型对象也会有自己的原型对象,一层一层向上,直到顶级原型对象null,这样就形成了一个原型链。

当访问对象的一个属性或方法时,当对象身上不存在该属性方法时,就会沿着原型链向上查找,直到查找到该属性方法位置。

原型链的顶层原型是Object.prototype,如果这里没有就只指向null

26.对作用域、作用域链的理解

作用域是一个变量或函数的可访问范围,作用域控制着变量或函数的可见性和生命周期。

  1. 全局作用域:可以全局访问
    • 最外层函数和最外层定义的变量拥有全局作用域
    • window上的对象属性方法拥有全局作用域
    • 为定义直接复制的变量自动声明拥有全局作用域
    • 过多的全局作用域变量会导致变量全局污染,命名冲突
  2. 函数作用域:只能在函数中访问使用
    • 在函数中定义的变量,都只能在内部使用,外部无法访问
    • 内层作用域可以访问外层,外层不能访问内存作用域
  3. ES6中的块级作用域:只在代码块中访问使用
    • 使用ES6中新增的letconst声明的变量,具备块级作用域,块级作用域可以在函数中创建(由{}包裹的代码都是块级作用域)
    • letconst申明的变量不会变量提升,const也不能重复声明
    • 块级作用域主要用来解决由变量提升导致的变量覆盖问题

作用域链: 变量在指定的作用域中没有找到,会依次向上一层作用域进行查找,直到全局作用域。这个查找的过程被称为作用域链。

27.浏览器的垃圾回收机制

垃圾回收JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不再参与运行时,就需要系统收回被占用的内存空间。如果不及时清理,会造成系统卡顿、内存溢出,这就是垃圾回收。

在 V8 中,会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放生存时间久的对象:

  • Major GC(主垃圾回收器)

    :主要负责老生代垃圾的回收

    • 内存占用比较小
  • Minor GC(副垃圾回收器)

    :主要负责新生代垃圾的回收

    • 对象的占用空间大 对象存活时间长
新生代(副垃圾回收器)

副垃圾回收器主要负责新⽣代的垃圾回收。大多数的对象最开始都会被分配在新生代,该存储空间相对较小,分为两个空间:from 空间(对象区)和 to 空间(空闲区)。

  • 新增变量会放到To空间,当空间满后需要执行一次垃圾清理操作
  • 对垃圾数据进行标记,标记完成后将存活的数据复制到From空间中,有序排列
  • 交换两个空间,原来的To变成From,旧的From变成To
老生代(主垃圾回收器)

主垃圾回收器主要负责⽼⽣代中的垃圾回收。存储一些占用空间大、存活时间长的数据,采用标记清除算法进行垃圾回收。

主要分为标记清除两个阶段。

  • 标记:将所有的变量打上标记0,然后从根节点(window对象、DOM树等)开始遍历,把存活的变量标记为1
  • 清除:清除标记为0的对象,释放内存。清除后将1的变量改为0,方便下一轮回收。

对⼀块内存多次执⾏标记清除算法后,会产⽣⼤量不连续的内存碎⽚。⽽碎⽚过多会导致⼤对象⽆法分配到⾜够的连续内存,于是⼜引⼊了另外⼀种算法——标记整理

标记整理的标记过程仍然与标记清除算法⾥的是⼀样的,先标记可回收对象,但后续步骤不是直接对可回收对象进⾏清理,⽽是让所有存活的对象都向⼀端移动,然后直接清理掉这⼀端之外的内存。

引用计数法

一个对象被引用一次,引用数就+1,反之就-1。当引用为0,就会出发垃圾回收。

这种方式会产生一个问题,在循环引用时,引用数永远不会为0,无法回收。

哪些情况会导致内存泄漏
  • 意外的全局变量:由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
  • 被遗忘的计时器或回调函数:设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
  • 脱离 DOM 的引用:获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
  • 闭包:不合理的使用闭包,从而导致某些变量一直被留在内存当中。

手写代码篇

1.数组去重

使用 Set

Set 是 JavaScript 中一个内建的对象,它只允许存储唯一的值。利用这一特性,我们可以轻松实现数组去重。

1
2
3
const arr = [1, 2, 3, 4, 4, 3, 2, 1];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr);
使用 filter

你也可以使用数组的 filter 方法来实现去重。这个方法会遍历数组,并对每个元素执行一个测试函数,只有使测试函数返回 true 的元素才会被保留在新数组中。

1
2
3
4
5
6
7
8
function uniqueArray(arr) {  
 return arr.filter((item, index) => arr.indexOf(item) === index);  
}  
 
const array = [1, 2, 3, 4, 4, 3, 2, 1];  
const unique = uniqueArray(array);  
console.log(unique); // 输出: [1, 2, 3, 4]

这个方法在处理大数组时可能效率不高,这个方法本身的时间复杂度就是 O(n)

使用 reduce

reduce 方法也可以用来实现数组去重。这个方法会遍历数组,并将每个元素归并成一个单一的结果。你可以利用这个结果来创建一个新数组,只包含唯一的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
function uniqueArray(arr) {  
 return arr.reduce((accumulator, current) => {  
   if (!accumulator.includes(current)) {  
     accumulator.push(current);  
  }  
   return accumulator;  
}, []);  
}  
 
const array = [1, 2, 3, 4, 4, 3, 2, 1];  
const unique = uniqueArray(array);  
console.log(unique); // 输出: [1, 2, 3, 4]

2.js将数字每千分位用逗号隔开

使用toLocaleString方法将数字格式化为带有千分位逗号的字符串。这个方法会根据你所在地区的本地化设置来格式化数字。

1
2
3
let num = 1234567.89;
let formattedNum = num.toLocaleString();
console.log(formattedNum);

你可以为toLocaleString方法提供一个选项对象,明确指定你想要的语言环境(locale)。例如,如果你想要使用美国英语的格式(即使用逗号作为千分位分隔符,点作为小数点),你可以这样做:

1
2
3
let num = 1234567.89;  
let formattedNum = num.toLocaleString('en-US');
console.log(formattedNum); // 输出 "1,234,567.89"

3.手写防抖和节流

防抖
1
2
3
4
5
6
7
8
9
10
function debounce(func, delay) {
let timer = null; // 创建一个变量来存储计时器ID
return function () {
if (timer) clearTimeout(timer);
timer = setTimeout(()=>{
func.call(this, delay);
timer = null;
}, 2000);
}
}
节流
1
2
3
4
5
6
7
8
9
10
function throttle(fn) {
let timer = null;
return function () {
if (timer) return timer;
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, 1000);
};
}

4.手写promise

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
class Promise{
constructor(executor){
this.state = 'pending';
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
let resolve = value => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onResolvedCallbacks.forEach(fn=>fn());
}
};
let reject = reason => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn=>fn());
}
};
try{
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled,onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
let promise2 = new Promise((resolve, reject) => {
if (this.state === 'fulfilled') {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
};
if (this.state === 'rejected') {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
};
if (this.state === 'pending') {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0)
});
};
});
return promise2;
}
catch(fn){
return this.then(null,fn);
}
}
function resolvePromise(promise2, x, resolve, reject){
if(x === promise2){
return reject(new TypeError('Chaining cycle detected for promise'));
}
let called;
if (x != null && (typeof x === 'object' || typeof x === 'function')) {
try {
let then = x.then;
if (typeof then === 'function') {
then.call(x, y => {
if(called)return;
called = true;
resolvePromise(promise2, y, resolve, reject);
}, err => {
if(called)return;
called = true;
reject(err);
})
} else {
resolve(x);
}
} catch (e) {
if(called)return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
//resolve方法
Promise.resolve = function(val){
return new Promise((resolve,reject)=>{
resolve(val)
});
}
//reject方法
Promise.reject = function(val){
return new Promise((resolve,reject)=>{
reject(val)
});
}
//race方法
Promise.race = function(promises){
return new Promise((resolve,reject)=>{
for(let i=0;i<promises.length;i++){
promises[i].then(resolve,reject)
};
})
}
//all方法(获取所有的promise,都执行then,把结果放到数组,一起返回)
Promise.all = function(promises){
let arr = [];
let i = 0;
function processData(index,data){
arr[index] = data;
i++;
if(i == promises.length){
resolve(arr);
};
};
return new Promise((resolve,reject)=>{
for(let i=0;i<promises.length;i++){
promises[i].then(data=>{
processData(i,data);
},reject);
};
});
}


Promise.defer = Promise.deferred = function () {
let dfd = {}
dfd.promise = new Promise((resolve,reject)=>{
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
}

module.exports = Promise;

5.手写深浅拷贝

assign实现浅拷贝
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function shallowCopy(obj){
if(obj !== Object || obj === null){
return obj;
}
return Object.assign({},obj);
}



let obj1 = {
name:'wwc',
age:'21',
favorite:{
game:'p系列',
hobby:'sleep',
}
}

let obj2 = shallowCopy(obj1);
obj1.favorite.hobby = 'eat';
console.log(obj2);
递归实现深拷贝
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
function deepClone(obj,clonedObjects = new WeakMap()){
// 如果 obj 为 null 或不是对象(包括基本数据类型),直接返回该值
if(obj === null || typeof obj !== 'object'){
return obj;
}
// 检查对象是否已经被克隆过,以避免循环引用导致的无限递归
if(clonedObjects.has(obj)){
return clonedObjects.get(obj);
}

let clone;
// 如果 obj 是数组
if(Array.isArray(obj)){
clone = [];
// 将原对象和克隆对象的引用存储在 WeakMap 中
clonedObjects,set(obj,clone);
// 递归克隆数组中的每个元素
for(let i = 0;i<obj.length;i++){
clone[i] = deepClone(obj[i],clonedObjects);
}
}

// 如果 obj 是普通对象
else if (obj instanceof Object){
clone = {};
// 将原对象和克隆对象的引用存储在 WeakMap 中
clonedObjects.set(obj,clone);
// 递归克隆对象的每个属性
for(let key of obj){
if(obj.hasOwnProperty(key)){
clone[key] = deepClone(obj[key],clonedObjects);
}
}
}

return clone;
}


const original = { a: 1, b: [2, 3], c: { d: 4 } };
const copy = deepClone(original);

console.log(copy); // 输出: { a: 1, b: [2, 3], c: { d: 4 } }
console.log(copy !== original); // true
console.log(copy.c !== original.c); // true
console.log(copy.b !== original.b); // true

6.手写ajax封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function ajax(url, method) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
//xhr.open('请求方式', '请求地址', 是否异步)
//初始化设置请求方法和url
xhr.open(method, url, true);
//发送
xhr.send();
//绑定事件,onreadystatechange,存储函数(或函数名)处理响应结果(每当 readyState 改变时,就会触发 onreadystatechange 事件,一共会触发 4 次,从 0 到 4)
//readyState 属性存有 XMLHttpRequest 的状态信息
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject(xhr.status);
}
}
}

})
}

7.交换a,b的值,不能用临时变量

扩展运算符
1
2
3
4
5
6
let a = 3;
let b = 6;

[a,b] = [b,a];

console.log(a,b);

8.new的实现

1
2
3
4
5
6
7
8
9
10
11
12
function myNew(obj,...args){

//1.创建一个空对象
const obj = {};
//2.将这个新对象的内部原型链接到构造函数的prototype对象
obj.__proto__ = constructor.prototype;
//3.将这个新对象作为this上下文,并调用构造函数
const result = constructor.apply(obj,...args);
// 4. 如果构造函数返回的是一个对象,则返回这个对象;否则返回新创建的对象
return result instanceof Object ? result:obj;

}

9.数组的扁平化

递归法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function flattenArray(arr) {  
   let result = [];  
   for (let i = 0; i < arr.length; i++) {  
       if (Array.isArray(arr[i])) {  
           result = result.concat(flattenArray(arr[i]));  
      } else {  
           result.push(arr[i]);  
      }  
  }  
   return result;  
}  
 
let nestedArray = [1, [2, [3, [4]], 5]];  
console.log(flattenArray(nestedArray)); // 输出 [1, 2, 3, 4, 5]

扩展运算符
1
2
3
4
5
6
7
8
9
function flatArray(arr){
while(arr.some(item => Array.isArray(item))){
arr = [].concat(...arr);
}
return arr
}

let nestedArray = [1, [2, [3, [4]], 5]];
console.log(flatArray(nestedArray));

10.函数柯里化

JavaScript中,你可以通过创建一个返回函数的函数来实现这种链式调用的模式。这种模式通常被称为柯里化(Currying)的一种形式,尽管在这个例子中,我们并没有减少参数的数量,而是延迟了它们的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function add(){
let sum = 0;

function Linkadd(num){
sum += num;
return Linkadd;
}
Linkadd.getsum = function(){
return sum;
}
return Linkadd;
}

let result = add(5)(3)(1);
console.log(result.getsum());

在这个例子中,add 函数返回了一个名为 innerAdd 的内部函数。innerAdd 函数接受一个数字参数,将其加到 sum 变量上,然后返回自己。因此,你可以连续调用 add(1)(2)(3),每次调用都会将新的数字添加到 sum 中。

需要注意的是,由于 innerAdd 是一个函数,如果你直接打印 add(1)(2)(3),它将输出函数本身而不是结果。

一个更好的方法是提供一个方法来获取结果,在这个版本中,我们添加了一个 getResult 方法来获取结果,而不是覆盖 toString。这样,innerAdd 函数的行为就更加清晰和可预测了。

ES6篇

1.let、const、var的区别

  • 块级作用域:

    块作用域由 { } 包裹,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:

    • 内层变量可能覆盖外层变量
    • 用来计数的循环变量泄露为全局变量
  • 变量提升: var存在变量提升,letconst不存在变量提升,即在变量只能在声明之后使用,否在会报错。

  • 给全局添加属性: 浏览器的全局对象是windowNode的全局对象是globalvar声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是letconst不会。

  • 重复声明: var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。constlet不允许重复声明变量。

  • 初始值设置: 在变量声明时,var let可以不用设置初始值。而const声明变量必须设置初始值。

  • 暂时性死区:在使用letconst命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。

2.箭头函数与普通函数的区别

  • 箭头函数是匿名函数,不能作为构造函数,使用new关键字。
  • 箭头函数没有arguments
  • 箭头函数没有自己的this,会获取所在的上下文作为自己的this
  • call()applay()bind()方法不能改变箭头函数中的this指向
  • 箭头函数没有prototype
  • 箭头函数不能用作Generator函数,不能使用yeild关键字

3.Set、Map的区别

Set

  • 创建: new Set([1, 1, 2, 3, 3, 4, 2])
  • add(value):添加某个值,返回Set结构本身。
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • has(value):返回一个布尔值,表示该值是否为Set的成员。
  • clear():清除所有成员,没有返回值。

Map

  • set(key, val):Map中添加新元素
  • get(key): 通过键值查找特定的数值并返回
  • has(key): 判断Map对象中是否有Key所对应的值,有返回true,否则返回false
  • delete(key): 通过键值从Map中移除对应的数据
  • clear(): 将这个Map中的所有元素删除

区别

  • Map是一种键值对的集合,和对象不同的是,键可以是任意值
  • Map可以遍历,可以和各种数据格式转换
  • Set是类似数组的一种的数据结构,类似数组的一种集合,但在Set中没有重复的值

4.map和Object的区别

mapObject都是用键值对来存储数据,区别如下:

  • 键的类型Map 的键可以是任意数据类型(包括对象、函数、NaN 等),而 Object 的键只能是字符串或者 Symbol 类型。
  • 键值对的顺序Map中的键值对是按照插入的顺序存储的,而对象中的键值对则没有顺序。
  • 键值对的遍例Map 的键值对可以使用 for...of 进行遍历,而 Object 的键值对需要手动遍历键值对。
  • 继承关系Map 没有继承关系,而 Object 是所有对象的基类。

5.map和weakMap的区别

它们是 JavaScript 中的两种不同的键值对集合,主要区别如下:

  1. map的键可以是任意类型,weakMap键只能是对象类型。
  2. map 使用常规的引用来管理键和值之间的关系,因此即使键不再使用,map 仍然会保留该键的内存。weakMap 使用弱引用来管理键和值之间的关系,因此如果键不再有其他引用,垃圾回收机制可以自动回收键值对。

6.说说你对Promise的理解

Promise是异步编程的一种解决方案,将异步操作以同步操作的流程表达出来,避免了地狱回调。

Promise的实例有三个状态:

  • Pending(初始状态)
  • Fulfilled(成功状态)
  • Rejected(失败状态)

Promise的实例有两个过程:

  • pending -> fulfilled : Resolved(已完成)

  • pending -> rejectedRejected(已拒绝)

    注意:一旦从进行状态变成为其他状态就永远不能更改状态了,其过程是不可逆的。

Promise构造函数接收一个带有resolvereject参数的回调函数。

  • resolve的作用是将Promise状态从pending变为fulfilled,在异步操作成功时调用,并将异步结果返回,作为参数传递出去
  • reject的作用是将Promise状态从pending变为rejected,在异步操作失败后,将异步操作错误的结果,作为参数传递出去

Promise的缺点:

  • 无法取消 Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

7.Promise方法

  • promise.then() 对应resolve成功的处理
  • promise.catch()对应reject失败的处理
  • promise.all()可以完成并行任务,将多个Promise实例数组,包装成一个新的Promise实例,返回的实例就是普通的Promise。有一个失败,代表该Primise失败。当所有的子Promise完成,返回值时全部值的数组
  • promise.race()类似promise.all(),区别在于有任意一个完成就算完成
  • promise.allSettled() 返回一个在所有给定的 promise 都已经 fulfilledrejected 后的 promise ,并带有一个对象数组,每个对象表示对应的promise 结果。

8.promise.all 和 promise.allsettled 区别

Promise.all()Promise.allSettled() 都是用来处理多个 Promise 实例的方法,它们的区别在于以下几点:

  • all: 只有当所有Promise实例都resolve后,才会resolve返回一个由所有Promise返回值组成的数组。如果有一个Promise实例reject,就会立即被拒绝,并返回拒绝原因。all是团队的成功才算,如果有一个人失败就算失败。
  • allSettled: 等所有Promise执行完毕后,不管成功或失败, 都会吧每个Promise状态信息放到一个数组里面返回。

9.对async/await 的理解

async/await其实是Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。通过async关键字声明一个异步函数, await 用于等待一个异步方法执行完成,并且会阻塞执行。 async函数返回的是一个 Promise 对象,如果在函数中return 一个变量,async会把这个直接量通过Promise.resolve()封装成Promise对象。如果没有返回值,返回Promise.resolve(undefined)

async/await对比Promise的优势
  • 代码可读性高,Promise虽然摆脱了回掉地狱,但自身的链式调用会影响可读性。
  • 相对Promise更优雅,传值更方便。
  • 对错误处理友好,可以通过try/catch捕获,Promise的错误捕获⾮常冗余

10.对ES6的理解

  • 解构赋值
  • 扩展运算符
  • 箭头函数
  • 模版字符串
  • SetMap集合
  • 新增class
  • Proxy
  • Promise

11.ES6模块和CommonJS模块有什么区别

语法不同:ES6 模块使用 importexport 关键字来导入和导出模块,而 CommonJS 模块使用 requiremodule.exportsexports 来导入和导出模块。

1
2
3
4
5
6
7
8
9
// ES6 模块
import { foo } from './module';
export const bar = 'bar';

// CommonJS 模块
const foo = require('./commonjs');
exports.bar = 'bar';


  • 异步加载: ES6 模块支持动态导入(dynamic import),可以异步加载模块。这使得在需要时按需加载模块成为可能,从而提高了性能。CommonJS 模块在设计时没有考虑异步加载的需求,通常在模块的顶部进行同步加载。

Vue篇

1.vue的常用的属性和指令有哪些?

属性
  • data:用于定义组件的初始数据。
  • props:用于传递数据给子组件。
  • computed:用于定义计算属性。
  • methods:用于定义组件的方法。
  • watch:用于监听组件的数据变化。
  • components:用于注册子组件。可以通过 components 属性将其他组件注册为当前组件的子组件,从而在模板中使用这些子组件。
指令:
v-text

v-text 指令,会把该元素下面的所有内容替换掉。

1
<div v-text="hello vue">hello world</div>

现实结果是:hello vue

v-html

v-html 指令,会用一个HTML标签字符串,替换该元素下面的所有内容。

但是,不建议使用v-html指令,因为它会导致被恶意者进行XSS攻击的潜在风险。

1
2
3
4
5
<div v-html="'<span style=&quot;color:red&quot;>hello vue</span>'">

hello world

</div>

现实结果是:字体颜色为红色的 hello vue

v-show

v-show 指令,控制元素的显示隐藏,元素存在并占据空间。

元素隐藏时,相当于给该元素添加了 CSS 样式:display:none;

1
2
3
<div v-show="show">hello vue</div>

<button @click="show = !show">changeShow</button>
v-if

v-if 指令,控制元素是否加载。

v-esle-if/v-else指令不能单独使用,必须配合v-if一起使用。

1
2
3
4
5
<div v-if="number===1">hello vue {{number}}</div>

<div v-else-if="number===2">hello world {{number}}</div>

<div v-else>hello someone {{number}}</div>
v-for

v-for 指令,for循环,基于源数据多次渲染元素或模板块。

v-for 既可以渲染一个数组,也可以渲染一个对象。

1
2
3
4
5
6
7
8
9
<div v-for="(item, idx) in [1, 2, 3]" :key="idx">

{{item}}

</div>
// 渲染的结果:
// 1
// 2
// 3
v-if 和v-for优先级

当 v-if 与 v-for 一起使用时:

在 vue2 中 v-for 比 v-if 有更高的优先级。这意味着 v-if 将分别重复运行于每个 v-for 循环中。
在 vue3 中 v-if 比 v-for 有更高的优先级。这意味着 v-if 的条件将无法访问到 v-for 作用域内定义的变量别名。

v-for 的key

①、为什么需要给 v-for 设置 key?

这牵扯到 vue 的 vnode 的 Diff 算法的特点

②、在 v-for 中直接用 index 作为 key 的值有什么不好?

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
<template>
<div v-for="(item, index) in list" :key="index" >{{item.name}}</div>
</template>
<script>
export default {
data() {
return {
list: [
{
id: 1,
name: "Person1"
},
{
id: 2,
name: "Person2"
},
{
id: 3,
name: "Person3"
},
{
id:4,
name:"Person4"
}]
}
},
}
</script>

此时,删除 “Person4” 是正常的,但是如果我删除 “Person2” 就会出现问题。

删除前

id index name
1 0 Person1
2 1 Person2
3 2 Person3
4 3 Person4

删除后

id index name
1 0 Person1
3 1 Person3
4 2 Person4

可见,数组的 index 下标始终是从 0 开始依次递增不间断的,当其中某一项被删除后,被删节点之后的 index 下标会自动全部做减 1 更新。所以,删除了 id 是 2 的节点时,被删节点之后的 index 下标全部做减 1 更新了。所以,当 DOM 内容比较复杂时,建议设置并使用唯一的 id 属性,来作为 key 的值。

v-on

v-on指令,可简写为“@”,绑定事件监听器。

1
<button v-on:click="number = number + 1">number++</button>

2.vue2的生命周期有哪些及每个生命周期做了什么?

创建前后:

  • beforeCreate(创建前): 数据观测和初始化事件还未开始,不能访问datacomputedwatchmethods上的数据方法。
  • created(创建后):实例创建完成,可以访问datacomputedwatchmethods上的数据方法,但此时渲染节点还未挂在到DOM上,所以不能访问。

挂载前后:

  • beforeMount(挂载前): Vue实例还未挂在到页面HTML上,此时可以发起服务器请求
  • mounted(挂载后):Vue实例已经挂在完毕,可以操作DOM

更新前后:

  • beforeUpdate(更新前): 数据更新之前调用,还未渲染页面
  • updated(更新后):DOM重新渲染,此时数据和界面都是新的。

销毁前后:

  • beforeDestorye(销毁前):实例销毁前调用,这时候能够获取到this
  • destoryed(销毁后):实例销毁后调用,实例完全被销毁。

3.MVVM的理解

MVVM是一种软件架构模式,MVVM 分为 ModelViewViewModel

  • Model代表数据模型,数据和业务逻辑都在Model层中定义;
  • View代表UI视图,负责数据的展示;
  • ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;

ModelView并无直接关联,而是通过ViewModel来进行联系的,ModelViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。

4.vue和react的区别,有什么相同

不同:

  • 模版语法不同,react采用JSX语法,vue使用基于HTML的模版语法
  • 数据绑定不同,vue 使用双向数据绑定,react 则需要手动控制组件的状态和属性。
  • 状态管理不同,vue使用vuex状态管理,react使用redux状态管理
  • 组件通信不同,vue使用props和事件的方式进行父子组件通信,react则通过props和回调函数的方式进行通信。
  • 生命周期不同,vue有8个生命周期钩子,react有10个
  • 响应式原理不同,vue使用双向绑定来实现数据更新,react则通过单向数据流来实现

相同

  • 组件化开发VueReact 都采用了组件化开发的方式,将用户界面划分为独立、可复用的组件,从而使代码更加模块化、可维护和可扩展。
  • 虚拟 DOMVueReact 都使用虚拟 DOM 技术,通过在 JavaScript 和真实 DOM 之间建立一个轻量级的虚拟 DOM 层,实现高效的 DOM 更新和渲染。
  • 响应式更新VueReact 都支持响应式更新,即当数据发生变化时,会自动更新相关的组件和视图,以保持用户界面的同步性。
  • 集成能力VueReact 都具有良好的集成能力,可以与其他库和框架进行整合,例如 Vue 可以与 VuexVue Router 等配套使用,React 可以与 ReduxReact Router 等配套使用。

5.Vue2和Vue3有哪些区别

  • Vue2使用的是optionsAPI Vue3使用composition API,更好的组织代码,提高代码可维护性
  • Vue3使用Proxy代理实现了新的响应式系统,比Vue2有着更好的性能和更准确的数据变化追踪能力。
  • Vue3引入了Teleprot组件,可以将DOM元素渲染到DOM数的其他位置,用于创建模态框、弹出框等。
  • Vue3全局API名称发生了变化,同时新增了watchEffectHooks等功能
  • Vue3TypeScript的支持更加友好
  • Vue3核心库的依赖更少,减少打包体积
  • 支持更好的Tree Shikng,可以更加精确的按需引入模块

6.SPA和多页面有什么区别

区别

  • 页面加载方式:在多页面应用中,每个页面都是独立的 HTML 文件,每次导航时需要重新加载整个页面。而在 SPA 中,初始加载时只加载一个 HTML 页面,后续的导航通过 JavaScript 动态地更新页面内容,无需重新加载整个页面。
  • 用户体验SPA 提供了流畅、快速的用户体验,因为页面切换时无需等待整个页面的重新加载,只有需要的数据和资源会被加载,减少了页面刷新的延迟。多页面应用则可能会有页面刷新的延迟,给用户带来较长的等待时间。
  • 代码复用SPA 通常采用组件化开发的方式,可以在不同的页面中复用组件,提高代码的可维护性和可扩展性。多页面应用的每个页面都是独立的,组件复用的机会较少。
  • 路由管理:在多页面应用中,页面之间的导航和路由由服务器处理,每个页面对应一个不同的 URL。而在 SPA 中,前端负责管理页面的导航和路由,通过前端路由库(如 React RouterVue Router)来管理不同路径对应的组件。
  • SEO(搜索引擎优化):由于多页面应用的每个页面都是独立的 HTML 文件,搜索引擎可以直接索引和抓取每个页面的内容,有利于搜索引擎优化。相比之下,SPA 的内容是通过 JavaScript 动态生成的,搜索引擎的爬虫可能无法正确地获取和索引页面的内容,需要采取额外的优化措施。
  • 服务器负载:SPA 只需初始加载时获取 HTMLCSSJavaScript 文件,后续的页面更新和数据获取通常通过 API 请求完成,减轻了服务器的负载。而多页面应用每次导航都需要从服务器获取整个页面的内容。

优点

  • 用户体验SPA 提供了流畅、快速的用户体验,在页面加载后,只有需要的数据和资源会被加载,减少了页面刷新的延迟。
  • 响应式交互:由于 SPA 依赖于异步数据加载和前端路由,可以实现实时更新和动态加载内容,使用户可以快速地与应用程序交互。
  • 代码复用SPA 通常采用组件化开发的方式,提高了代码的可维护性和可扩展性。
  • 服务器负载较低:由于只有初始页面加载时需要从服务器获取 HTMLCSSJavaScript 文件,减轻了服务器的负载。

缺点

  • 首次加载时间SPA 首次加载时需要下载较大的 JavaScript 文件,这可能导致初始加载时间较长。
  • SEO(搜索引擎优化)问题:由于 SPA 的内容是通过 JavaScript 动态生成的,搜索引擎的爬虫可能无法正确地获取和索引页面的内容。
  • 内存占用SPA 在用户浏览应用程序时保持单个页面的状态,这可能导致较高的内存占用。
  • 安全性:由于 SPA 通常使用 API 进行数据获取,因此需要特别注意安全性。

7.Vue的性能优化有哪些

编码阶段
  • v-ifv-for不一起使用
  • v-for保证key的唯一性
  • 使用keep-alive缓存组件
  • v-ifv-show酌情使用
  • 路由懒加载、异步组件
  • 图片懒加载
  • 节流防抖
  • 第三方模块按需引入
  • 服务端与渲染
打包优化
  • 压缩代码
  • 使用CDN加载第三方模块
  • 抽离公共文件
用户体验
  • 骨架屏
  • 客户端缓存
SEO优化
  • 预渲染
  • 服务端渲染
  • 合理使用 meta 标签

8.Computed 和 Watch 的区别

computed计算属性,通过对已有的属性值进行计算得到一个新值。它需要依赖于其他的数据,当数据发生变化时,computed会自动计算更新。computed属性值会被缓存,只有当依赖数据发生变化时才会重新计算,这样可以避免重复计算提高性能。

watch用于监听数据的变化,并在变化时执行一些操作。它可以监听单个数据或者数组,当数据发生变化时会执行对应的回调函数,和computed不同的是watch不会有缓存。

9.Vue组件通信

父传子

  • props
  • $children
  • $refs

子传父

  • $emit
  • $parent

兄弟组件

  • provied
  • inject
  • eventBus
  • Vuex

10.常见的事件修饰符及其作用

  • .stop阻止冒泡
  • .prevent阻止默认事件
  • .capture 与事件冒泡的方向相反,事件捕获由外到内;
  • .self 只会触发自己范围内的事件,不包含子元素;
  • .once只会触发一次。

11.v-if和v-show的区别

v-if元素不可见,直接删除DOM,有更高的切换消耗。

v-show通过设置元素display: none控制显示隐藏,更高的初始渲染消耗。

12.Vue中data为什么是一个函数而不是对象

因为对象是一个引用类型,如果data是一个对象的情况下会造成多个组件共用一个datadata为一个函数,每个组件都会有自己的私有数据空间,不会干扰其他组件的运行。

13.mixin 和 mixins 区别

  • mixin 是一个包含可复用代码块的对象,可以包含数据、方法、生命周期钩子等。
  • mixins 是 Vue 组件选项中的一个数组属性,用于指定一个或多个 mixin 对象,从而将这些 mixin 混入组件中。
mixin的选项合并(核心概念)
  • 什么是选项合并:

(面试问题)当组件和混⼊对象含有同名选项时,这些选项将以恰当的⽅式进⾏“合并”。

1.混入的数据存在冲突时

数据对象在内部会进⾏递归合并,并在发⽣冲突时以组件数据优先。

2.混入的生命周期存在冲突时

同名钩子函数将合并为一个数组,因此都将被调用。 另外,混入对象的钩子将在组件自身钩子之前调用。

3.混入的方法存在冲突时

值为对象的选项,将被合并为同一个对象。 例如 methodscomponentsdirectives

两个对象键名冲突时,取组件对象的键值对。(2个同名的方法,取组件内的而不是mixins中的)

mixin的缺点
1.变量来源不明确(隐式传入),不利于阅读,使代码变得难以维护。

组件里可以引入多个mixin,并直接隐式调用mixin里的变量/方法,这会让我们有时候混乱,区分不出这些变量和方法 分别是哪个mixin里的?

2.多个mixins的生命周期会融合到一起运行,但是同名属性、同名方法无法融合,可能会导致冲突或覆盖。

比如组件1中的方法要输出属性info,
但是组件2中也有同名属性info,且覆盖了组件1中的属性info,
那么当执行组件1中的方法时,输出的却是组件2中的属性,
这个我们可以避免,但是一不小心就会导致冲突,很容易制造混乱。

3.mixins和组件可能出现多对多的关系,复杂度较高

即一个组件可以引用多个mixins,一个mixins也可以被多个组件引用。

示例一:

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
// 定义一个 mixin 对象
const myMixin = {
data() {
return {
mixinData: '这是来自 mixin 的数据'
};
},
created() {
console.log('Mixin 的 created 钩子被调用');
},
methods: {
mixinMethod() {
console.log('这是来自 mixin 的方法');
}
}
};

// 使用 mixin 的组件
new Vue({
mixins: [myMixin],
data() {
return {
componentData: '这是组件自己的数据'
};
},
created() {
console.log('组件的 created 钩子被调用');
},
methods: {
componentMethod() {
console.log('这是组件自己的方法');
}
}
});


示例二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const mixinA = {
created() {
console.log('mixinA 的 created 钩子');
}
};

const mixinB = {
created() {
console.log('mixinB 的 created 钩子');
}
};

// 使用多个 mixin 的组件
new Vue({
mixins: [mixinA, mixinB],
created() {
console.log('组件的 created 钩子');
}
});

14.使用 Object.defineProperty() 来进行数据劫持有什么缺点?

在 Vue 2 中,Object.defineProperty() 被用于实现数据劫持(reactive data binding),使得数据变动时能够触发更新。然而,Object.defineProperty() 存在一些局限性和缺点,这些缺点是促使 Vue 3 采用 Proxy 实现响应式系统的原因之一。以下是 Object.defineProperty() 的主要缺点:

1. 无法监听数组索引的变动

Object.defineProperty() 无法直接监听数组索引的变动。例如,通过索引直接修改数组元素或改变数组长度,这些操作不会触发相应的 setter,从而无法响应这些变化。

示例

1
2
3
4
5
6
7
8
9
10
const data = { items: [1, 2, 3] };

Object.defineProperty(data, 'items', {
set(newValue) {
console.log('Items changed');
}
});

data.items[0] = 10; // 不会触发 setter
data.items.length = 0; // 不会触发 setter
2. 无法检测新增或删除属性

Object.defineProperty() 只能劫持对象已有的属性,对于动态添加的新属性或删除的属性,无法进行劫持和检测。这意味着对对象的属性添加或删除操作不会自动触发视图更新。

示例

1
2
3
4
5
6
7
8
9
10
const data = { name: 'Vue' };

Object.defineProperty(data, 'name', {
set(newValue) {
console.log('Name changed');
}
});

data.age = 25; // 无法检测到新增属性
delete data.name; // 无法检测到删除属性
3. 手动递归劫持

为了使整个对象的所有属性都响应式,需要手动递归遍历每个属性并对其进行劫持,处理深层嵌套的对象非常麻烦,容易导致性能问题。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function reactive(obj) {
if (typeof obj !== 'object' || obj === null) return;

for (let key in obj) {
if (obj.hasOwnProperty(key)) {
Object.defineProperty(obj, key, {
get() {
// getter logic
},
set(newValue) {
// setter logic
}
});

reactive(obj[key]); // 递归
}
}
}

const data = { nested: { value: 'hello' } };
reactive(data);
4. 性能问题

对于大型对象,递归遍历和劫持所有属性会导致初始化性能较差。每次属性访问都需要通过 getter 和 setter 拦截,增加了访问开销。

Vue 3 中的改进:Proxy

为了克服上述缺点,Vue 3 采用了 Proxy 来实现响应式系统。Proxy 可以监听和拦截几乎所有对象的操作,包括属性读取、写入、删除、新增等,具有更强的灵活性和性能优势。

Proxy 的优势

  1. 监听数组索引变动Proxy 可以监听数组索引的变动和长度变化。
  2. 检测新增和删除属性Proxy 可以拦截对象属性的添加和删除操作。
  3. 无需递归Proxy 可以在属性访问时动态拦截,无需在初始化时递归遍历所有属性。
  4. 性能更好:对于大型对象,Proxy 提供了更好的性能,避免了初始化时的深度遍历。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const data = { items: [1, 2, 3], name: 'Vue' };

const handler = {
get(target, prop) {
console.log(`Getting ${prop}`);
return Reflect.get(target, prop);
},
set(target, prop, value) {
console.log(`Setting ${prop} to ${value}`);
return Reflect.set(target, prop, value);
}
};

const proxyData = new Proxy(data, handler);

proxyData.items[0] = 10; // 触发 set 拦截器
proxyData.age = 25; // 触发 set 拦截器
delete proxyData.name; // 触发 deleteProperty 拦截器
总结

Object.defineProperty() 在 Vue 2 中的使用有其局限性,包括无法监听数组索引变动、无法检测属性新增或删除、需要手动递归劫持、性能问题和维护复杂性。Vue 3 通过使用 Proxy 来实现更强大和灵活的响应式系统,有效解决了这些问题,提高了代码的可维护性和性能。

15.Vue是如何收集依赖的?

依赖收集发生在defineReactive()方法中,在方法内new Dep()实例化一个Dep()实例,然后在getter中通过dep.depend()方法对数据依赖进行收集,然后在settter中通过dep.notify()通知更新。整个Dep其实就是一个观察者,把收集的依赖存储起来,在需要的时候进行调用。在收集数据依赖的时候,会为数据创建一个Watcher,当数据发生改变通知每个Watcher,由Wathcer进行更新渲染。

16.slot是什么?有什么作用?原理是什么?

slot插槽,一般在封装组件的时候使用,在组件内不知道以那种形式来展示内容时,可以用slot来占据位置,最终展示形式由父组件以内容形式传递过来,主要分为三种:

  • 默认插槽:又名匿名插槽,当slot没有指定name属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。
  • 具名插槽:带有具体名字的插槽,也就是带有name属性的slot,一个组件可以出现多个具名插槽。
  • 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。

实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到slot标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。

17双向数据绑定的原理

采用数据劫持结合发布者-订阅者模式的方式,data数据在初始化的时候,会实例化一个Observe类,在它会将data数据进行递归遍历,并通过Object.defineProperty方法,给每个值添加上一个getter和一个setter。在数据读取的时候会触发getter进行依赖(Watcher)收集,当数据改变时,会触发setter,对刚刚收集的依赖进行触发,并且更新watcher通知视图进行渲染。

18.$nextTick 原理及作用

$nextTick 是一个常用的方法,它的主要作用是确保在下一次 DOM 更新循环结束后执行一个回调函数。这对于在数据变化后立即获取更新后的 DOM 状态非常有用。以下是对 $nextTick 的详细解释,包括其原理及作用。

作用
  1. 确保在 DOM 更新后执行代码: 当 Vue 中的数据发生变化时,DOM 更新是异步的,Vue 会在下一个事件循环中进行批量更新。$nextTick 可以确保在数据更新并且 DOM 重新渲染后执行某些操作。
  2. 访问更新后的 DOM 元素: 在一些情况下,需要在数据更新后立即对 DOM 元素进行操作,例如获取元素的高度、宽度,或是执行某些与 DOM 相关的操作。使用 $nextTick 可以确保这些操作在 DOM 更新完成后执行。
使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default {
data() {
return {
message: 'Hello Vue!'
};
},
methods: {
updateMessage() {
this.message = 'Hello Vue.js!';
this.$nextTick(() => {
// 确保 DOM 更新完成后执行
console.log(this.$refs.messageRef.textContent); // 获取更新后的 DOM 内容
});
}
}
};
1
2
3
4
5
6
<template>
<div>
<p ref="messageRef">{{ message }}</p>
<button @click="updateMessage">Update Message</button>
</div>
</template>

在这个示例中,当 updateMessage 方法被调用时,message 的值会改变,但由于 DOM 更新是异步的,如果立即访问 this.$refs.messageRef.textContent,可能会得到更新前的内容。通过 $nextTick 确保在 DOM 更新完成后访问 DOM 元素。

19.Vue中怎么重置data?

1
Oject.assign(this.$data,this.$options.data())

this.$data 是获取当前状态下的data

this.$options.data() 是获取该组件初始状态下的data

Object.assign 用于将所有可枚举属性的值从一个或多个源对象复制到目标对象

20.Vue中data的属性可以和methods中的方法同名吗?为什么?

不可以。在initState的时候,会对data中的属性、props、methods的属性名进行检查。出现同名会立即警告

源码 中的 initData() 方法

1
2
if (methods && hasOwn(methods, key)) { warn(` Method "${key}" has already been defined as a data property.`, vm ) }

会取出 methods 中的方法进行判断,也就是 hasOwn(methods, key)

如果此 key 值 在 methods 中存在,会有warn 警告

为什么会先执行data中的呢?因为是先init data的

21.对keep-alive的理解

keep-alive是Vue提供的一个抽象组件,用来对组件进行缓存,从而节省性能,由于是一个抽象组件,所以在页面渲染完毕后不会被渲染成一个DOM元素,所以再次回到上一个页面,该页面状态会保持,不会重新渲染。

  • include 字符串或正则表达式,只有名称匹配的组件会被匹配;
  • exclude 字符串或正则表达式,任何名称匹配的组件都不会被缓存;
  • max 数字,最多可以缓存多少组件实例。

2 个生命周期 activated , deactivated

  • activated:当缓存的组件被激活时,该钩子函数被调用。可以在该钩子函数中进行一些状态恢复、数据更新等操作。
  • deactivated:当缓存的组件被停用时,该钩子函数被调用。可以在该钩子函数中进行一些状态保存、数据清理等操作。

22.v-for循环中key有什么作用

key是虚拟DOM对象的标识,当数据发生变化时,Vue会根据【新数据】生成【新的虚拟DOM】, 随后Vue进行【新虚拟DOM】与【旧虚拟DOM】的差异比较。

对比规则
(1)旧虚拟DOM中找到了与新虚拟DOM相同的key:

①.若虚拟DOM中内容没变, 直接使用之前的真实DOM!

②.若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM。

(2).旧虚拟DOM中未找到与新虚拟DOM相同的key创建新的真实DOM,随后渲染到到页面。

用index作为key可能会引发的问题:若对数据进行:逆序添加、逆序删除等破坏顺序操作: 会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。如果结构中还包含输入类的DOM:会产生错误DOM更新 ==> 界面有问题。

最好使用每条数据的唯一标识作为key, 比如id、手机号、身份证号、学号等唯一值。如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。

在 Vue 的 v-for 循环中,key 是一个特殊的属性,用于指定每个被迭代的元素的唯一标识。它的作用有以下几个方面:

作用高效的更新策略

​ key 的主要作用是帮助 Vue 在进行列表渲染时,识别每个元素的身份。Vue 使用 key 来跟踪每个节点的身份,从而在进行列表更新时,尽可能地复用和更新现有的 DOM 节点,减少不必要的 DOM 操作,提高性能。

维持组件状态在使用 v-for 渲染组件列表时,每个组件都是独立存在的,拥有自己的状态

​ 使用 key 可以确保在列表更新时,每个组件都能保持自己的状态,而不会出现错位或混淆的情况。

提供可靠的唯一性

​ 使用具有唯一性的 key 可以避免在列表中存在相同的项或出现重复的项。它帮助 Vue 在循环中区分不同的元素,确保每个元素都有一个独特的标识

23.vue如何监听键盘事件

  1. @keyup. 事件修饰符方法

    1
    2
    3
    <template>
    <input ref="myInput" type="text" value="hello world" autofocus @keyup.enter="handleKey">
    </template>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <template>
    <input ref="myInput" type="text" value="hello world" autofocus @keyup.enter="handleKey">
    </template>
    <script>
    export default {
    methods: {
    handleKey(e) {
    console.log(e)
    }
    }
    }
    </script>
  2. addEventListener

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <script>
    export default {
    mounted() {
    document.addEventListener('keyup', this.handleKey)
    },
    beforeDestroy() {
    document.removeEventListener('keyup', this.handleKey)
    },
    methods: {
    handleKey(e) {
    console.log(e)
    }
    }
    }
    </script>

24.你知道v-model的原理吗?

25.vue首页白屏是什么问题引起的?如何解决呢?

打包优化 路由懒加载 代码压缩 第三方插件按需加载

在考虑性能优化方面,可以采取以下方法来减少 Vue 应用程序的首页白屏时间:

  1. 代码分割和懒加载:将应用程序拆分为更小的模块,并使用 Vue 的异步组件功能或者动态 import 来实现懒加载,这样可以减少首次加载时需要下载的代码量,加快页面渲染速度。

  2. 路由懒加载:对于使用了 Vue 路由的应用程序,可以将路由配置进行懒加载,只在需要时加载对应的组件,而不是一次性加载所有路由组件。

  3. 代码压缩和混淆:使用工具(如 Webpack)对代码进行压缩和混淆,减小文件大小,加快资源加载速度。

  4. 图片优化:对于页面中的图片资源,使用适当的压缩和优化方法,减小图片文件大小,从而减少页面加载时间。

  5. 缓存优化:合理使用浏览器缓存和服务端缓存,减少重复请求,加快页面加载速度。

  6. CDN 加速:将静态资源部署到 CDN 上,利用 CDN 的分布式网络加速资源加载,提高页面加载速度。

  7. 减少 HTTP 请求:尽量减少页面需要发起的 HTTP 请求,合并和精简资源文件,减少页面加载时间。

  8. 服务端渲染(SSR):对于需要 SEO 优化或者有较高首屏渲染要求的应用程序,考虑使用服务端渲染来提高页面加载性能和搜索引擎索引效果。

通过综合使用这些方法,可以有效减少 Vue 应用程序的首页白屏时间,提升用户体验和性能表现。

26.CDN加速

当使用 CDN(内容分发网络)加速 Vue 应用程序时,可以将静态资源(如 JavaScript 文件、CSS 文件、图片等)部署到 CDN 上,从而利用 CDN 的分布式网络加速资源加载,提高页面加载速度。

举个例子,假设你的 Vue 应用程序包含以下静态资源:

  1. JavaScript 文件app.js
  2. CSS 文件styles.css
  3. 图片文件logo.png

你可以将这些静态资源上传到一个 CDN 服务提供商(如 Cloudflare、Amazon CloudFront、Azure CDN 等)上,并获得对应的 CDN 地址,例如:

  • JavaScript 文件的 CDN 地址:https://cdn.example.com/app.js
  • CSS 文件的 CDN 地址:https://cdn.example.com/styles.css
  • 图片文件的 CDN 地址:https://cdn.example.com/logo.png

然后,在 Vue 应用程序的 HTML 文件中,将这些静态资源的链接指向 CDN 地址,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue App</title>
<link rel="stylesheet" href="https://cdn.example.com/styles.css">
</head>
<body>
<div id="app"></div>

<script src="https://cdn.example.com/app.js"></script>
</body>
</html>

这样,在用户访问你的 Vue 应用程序时,静态资源会通过 CDN 进行加速,从最接近用户的 CDN 节点进行传输,而不是直接从你的服务器加载,从而减少了网络延迟,加快了资源加载速度,提高了页面加载性能。

27.服务端渲染(SSR)

以下是一个完整的例子,展示如何在 Vue 应用程序中使用服务端渲染:

  1. 创建 Vue 应用程序:首先,创建一个 Vue 应用程序,例如使用 Vue CLI 来初始化一个新项目。

  2. 配置服务器端入口:在项目中创建服务器端入口文件,通常命名为 server.js,并使用 Vue Server Renderer 实现服务器端渲染。以下是一个简单的示例:

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
// server.js

const express = require('express');
const fs = require('fs');
const path = require('path');
const { createBundleRenderer } = require('vue-server-renderer');

const server = express();
const template = fs.readFileSync(path.resolve(__dirname, './index.template.html'), 'utf-8');
const serverBundle = require('./dist/server-bundle.json');
const clientManifest = require('./dist/client-manifest.json');

const renderer = createBundleRenderer(serverBundle, {
runInNewContext: false,
template,
clientManifest
});

server.use(express.static(path.resolve(__dirname, './dist')));

server.get('*', async (req, res) => {
const context = { url: req.url };

try {
const html = await renderer.renderToString(context);
res.send(html);
} catch (error) {
res.status(500).send('Internal Server Error');
}
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
  1. 创建模板文件:创建一个 HTML 模板文件 index.template.html,用于包裹 Vue 应用程序的内容,并提供一个占位符用于 Vue Server Renderer 插入渲染后的 HTML。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- index.template.html -->

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue SSR App</title>
<!-- 这里可以插入一些通用的 meta 标签、样式表等 -->
</head>
<body>
<!-- 这里是 Vue 应用程序的根节点 -->
<!-- Vue Server Renderer 会将渲染后的 HTML 插入到这个 div 中 -->
<div id="app">{{ APP }}</div>
<!-- 这里可以插入一些通用的脚本 -->
</body>
</html>
  1. 构建应用程序:使用 Vue CLI 或其他构建工具来构建 Vue 应用程序。确保生成的客户端和服务器端的 bundle 文件。

  2. 运行服务器端:在终端中运行服务器端代码 node server.js,启动服务器。

通过以上步骤,你的 Vue 应用程序就会在服务器端进行渲染,并将渲染后的 HTML 页面发送给客户端。这样客户端收到的就是一个已经包含了初始内容的完整 HTML 页面,而不是一个空白的页面,从而提高了首屏加载速度和 SEO 效果。

28.vue-router是用来做什么的?它有哪些组件?

vue-router路由,通俗来讲主要是来实现页面的跳转,通过设置不同的path,向服务器发送的不同的请求,获取不同的资源。
主要组件:router-view、router-link

29.vue-router钩子函数有哪些?都有哪些参数?

Vue Router 提供了一系列的导航钩子函数,用于在路由导航过程中执行一些操作,例如权限验证、路由拦截等。常用的导航钩子函数包括:

  1. 全局前置守卫beforeEach(to, from, next)

    • 参数:
      • to: Route: 即将要进入的目标路由对象
      • from: Route: 当前导航正要离开的路由
      • next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
  2. 全局解析守卫beforeResolve(to, from, next)

    • 参数与全局前置守卫相同。
  3. 全局后置钩子afterEach(to, from)

    • 参数:
      • to: Route: 进入的目标路由对象
      • from: Route: 离开的路由对象
  4. 路由独享的守卫

    • beforeEnter(to, from, next): 在单个路由配置中独享的导航守卫。
      • 参数与全局前置守卫相同。
  5. 组件内的守卫

    • beforeRouteEnter(to, from, next): 进入路由前,在路由导航被确认前调用。
    • beforeRouteUpdate(to, from, next): 路由更新时调用。
    • beforeRouteLeave(to, from, next): 离开路由时调用。

以上是常用的 Vue Router 导航钩子函数及其参数。这些钩子函数允许你在路由导航的不同阶段执行一些逻辑,并控制导航的行为。

30.route和router有什么区别?

route:代表当前路由信息对象,可以获取到当前路由的信息参数
router:代表路由实例的对象,包含了路由的跳转方法,钩子函数等

31.setup()函数

1.基本使用

使用响应式 API 来声明响应式的状态,在 setup() 函数中返回的对象会暴露给模板和组件实例。其它的选项也可以通过组件实例来获取 setup() 暴露的属性

在模板中访问从 setup 返回的 ref 时,它会自动浅层解包,因此你无须再在模板中为它写 .value。当通过 this 访问时也会同样如此解包。

setup() 自身并不含对组件实例的访问权,即在 setup() 中访问 this 会是 undefined。你可以在选项式 API 中访问组合式 API 暴露的值,但反过来则不行。

2.访问 Prop

setup 函数的第一个参数是组件的 props。和标准的组件一致,一个 setup 函数的 props 是响应式的,并且会在传入新的 props 时同步更新。

注意如果你解构了 props 对象,解构出的变量将会丢失响应性。

推荐通过 props.xxx 的形式来使用其中的 props。

如果确实需要解构 props 对象,或者需要将某个 prop 传到一个外部函数中并保持响应性,那么你可以使用 toRefs()toRef() 这两个工具函数:

3.Setup的上下文

传入 setup 函数的第二个参数是一个 Setup 上下文对象。上下文对象暴露了其他一些在 setup 中可能会用到的值:

该上下文对象是非响应式的,可以安全地解构:

attrs 和 slots 都是有状态的对象,它们总是会随着组件自身的更新而更新。这意味着你应当避免解构它们,并始终通过 attrs.x 或 slots.x 的形式使用其中的属性。此外还需注意,和 props 不同,attrs 和 slots 的属性都不是响应式的。如果你想要基于 attrs 或 slots 的改变来执行副作用,那么你应该在 onBeforeUpdate 生命周期钩子中编写相关逻辑。

expose 函数用于显式地限制该组件暴露出的属性,当父组件通过模板引用访问该组件的实例时,将仅能访问 expose 函数暴露出的内容

在父组件通过ref获取子组件的实例的属性和方法的需求中,需要注意:

1.如果子组件是 选项式API组件,基本不需要做任何操作

2.如果子组件是 组合式API组件,需要通过 context.expose 暴露给父组件需要使用的属性和方法

3.如果父组件使用 选项式API, 可以通过 this.$refs.refName 访问到子组件想要你看到的属性和方法

4.如果父组件使用 组合式API,需要在setup中先创建 refName,然后再访问子组件想要你看到的属性和方法(const refName = ref() refName.value.X)

4.与渲染函数一起使用

setup 也可以返回一个渲染函数,此时在渲染函数中可以直接使用在同一作用域下声明的响应式状态:

返回一个渲染函数将会阻止我们返回其他东西。对于组件内部来说,这样没有问题,但如果我们想通过模板引用将这个组件的方法暴露给父组件,那就有问题

我们可以通过调用 expose() 解决这个问题:

32.响应式原理

Vue2.x是借助Object.defineProperty()实现的,而Vue3.x是借助Proxy实现的

我们通过Object.defineProperty为对象obj添加属性,可以设置对象属性的gettersetter函数。之后我们每次通过点语法获取属性都会执行这里的getter函数,在这个函数中我们会把调用此属性的依赖收集到一个集合中 ;而在我们给属性赋值(修改属性)时,会触发这里定义的setter函数,在次函数中会去通知集合中的依赖更新,做到数据变更驱动视图变更。

3.x的与2.x的核心思想一致,只不过数据的劫持使用Proxy而不是Object.defineProperty,只不过Proxy相比Object.defineProperty在处理数组和新增属性的响应式处理上更加方便。

33.router和route的区别

  • $route 是路由信息,包括pathparamsqueryname等路由信息参数
  • $router 是路由实例,包含了路由跳转方法、钩子函数等

34.如何设置动态路由

  • params传参
    • 路由配置: /index/:id
    • 路由跳转:this.$router.push({name: 'index', params: {id: "zs"}});
    • 路由参数获取:$route.params.id
    • 最后形成的路由:/index/zs
  • query传参
    • 路由配置:/index正常的路由配置
    • 路由跳转:this.$rouetr.push({path: 'index', query:{id: "zs"}});
    • 路由参数获取:$route.query.id
    • 最后形成的路由:/index?id=zs

区别

  • 获取参数方式不一样,一个通过$route.params,一个通过 $route.query
  • 参数的生命周期不一样,query参数在URL地址栏中显示不容易丢失,params参数不会在地址栏显示,刷新后会消失

35.虚拟DOM的理解

对虚拟DOM的理解

虚拟DOM就是用JS对象来表述DOM节点,是对真实DOM的一层抽象。可以通过一些列操作使这个棵树映射到真实DOM上。

如在Vue中,会把代码转换为虚拟DOM,在最终渲染到页面,在每次数据发生变化前,都会缓存一份虚拟DOM,通过diff算法来对比新旧虚拟DOM记录到一个对象中按需更新,最后创建真实DOM,从而提升页面渲染性能。

虚拟DOM就一定比真实DOM更快吗

虚拟DOM不一定比真实DOM更快,而是在特定情况下可以提供更好的性能。

在复杂情况下,虚拟DOM可以比真实DOM操作更快,因为它是在内存中维护一个虚拟的DOM树,将真实DOM操作转换为对虚拟DOM的操作,然后通过diff算法找出需要更新的部分,最后只变更这部分到真实DOM就可以。在频繁变更下,它可以批量处理这些变化从而减少对真实DOM的访问和操作,减少浏览器的回流重绘,提高页面渲染性能。

而在一下简单场景下,直接操作真实DOM可能会更快,当更新操作很少或者只是局部改变时,直接操作真实DOM比操作虚拟DOM更高效,省去了虚拟DOM的计算、对比开销。

虚拟DOM的解析过程
  • 首先对将要插入到文档中的 DOM 树结构进行分析,使用 js 对象将其表示出来,比如一个元素对象,包含 TagNamepropsChildren 这些属性。然后将这个 js 对象树给保存下来,最后再将 DOM 片段插入到文档中。
  • 当页面的状态发生改变,需要对页面的 DOM 的结构进行调整的时候,首先根据变更的状态,重新构建起一棵对象树,然后将这棵新的对象树和旧的对象树进行比较,记录下两棵树的的差异。
  • 最后将记录的有差异的地方应用到真正的 DOM 树中去,这样视图就更新了。

36.DIFF算法原理

diff的目的是找出差异,最小化的更新视图。 diff算法发生在视图更新阶段,当数据发生变化的时候,diff会对新旧虚拟DOM进行对比,只渲染有变化的部分。

  1. 对比是不是同类型标签,不是同类型直接替换
  2. 如果是同类型标签,执行patchVnode方法,判断新旧vnode是否相等。如果相等,直接返回。
  3. 新旧vnode不相等,需要比对新旧节点,比对原则是以新节点为主,主要分为以下几种。
    1. newVnodeoldVnode都有文本节点,用新节点替换旧节点。
    2. newVnode有子节点,oldVnode没有,新增newVnode的子节点。
    3. newVnode没有子节点,oldVnode有子节点,删除oldVnode中的子节点。
    4. newVnodeoldVnode都有子节点,通过updateChildren对比子节点。
双端diff

updateChildren方法用来对比子节点是否相同,将新旧节点同级进行比对,减少比对次数。会创建4个指针,分别指向新旧两个节点的首尾,首和尾指针向中间移动。

每次对比下两个头指针指向的节点、两个尾指针指向的节点,头和尾指向的节点,是不是 key是一样的,也就是可复用的。如果是重复的,直接patch更新一下,如果是头尾节点,需要进行移动位置,结果以新节点的为主。

如果都没有可以复用的节点,就从旧的vnode中查找,然后进行移动,没有找到就插入一个新节点。

当比对结束后,此时新节点还有剩余,就批量增加,如果旧节点有剩余就批量删除。

通用

1.从输入URL到页面加载的全过程

首先在浏览器中输入URL

查找缓存:浏览器先查看浏览器缓存-系统缓存-路由缓存中是否有该地址页面,如果有则显示页面内容。如果没有则进行下一步。

  • 浏览器缓存:浏览器会记录DNS一段时间,因此,只是第一个地方解析DNS请求;
  • 操作系统缓存:如果在浏览器缓存中不包含这个记录,则会使系统调用操作系统, 获取操作系统的记录(保存最近的DNS查询缓存);
  • 路由器缓存:如果上述两个步骤均不能成功获取DNS记录,继续搜索路由器缓存;
  • ISP缓存:若上述均失败,继续向ISP搜索。

DNS域名解析:浏览器向DNS服务器发起请求,解析该URL中的域名对应的IP地址。DNS服务器是基于UDP的,因此会用到UDP协议

建立TCP连接:解析出IP地址后,根据IP地址和默认80端口,和服务器建立TCP连接

发起HTTP请求:浏览器发起读取文件的HTTP请求,该请求报文作为TCP三次握手的第三次数据发送给服务器

服务器响应请求并返回结果:服务器对浏览器请求做出响应,并把对应的html文件发送给浏览器

关闭TCP连接:通过四次挥手释放TCP连接

浏览器渲染:客户端(浏览器)解析HTML内容并渲染出来

2.浏览器解析页面的过程

构建DOM树:词法分析然后解析成DOM树(dom tree),是由dom元素及属性节点组成,树的根是document对象

构建CSS规则树:生成CSS规则树(CSS Rule Tree)

构建render树:Web浏览器将DOM和CSSOM结合,并构建出渲染树(render tree)

布局(Layout):计算出每个节点在屏幕中的位置

绘制(Painting):即遍历render树,并使用UI后端层绘制每个节点。

3.JS引擎解析过程

创建window对象:window对象也叫全局执行环境,当页面产生时就被创建,所有的全局变量和函数都属于window的属性和方法,而DOM Tree也会映射在window的doucment对象上。当关闭网页或者关闭浏览器时,全局执行环境会被销毁。

加载文件:完成js引擎分析它的语法与词法是否合法,如果合法进入预编译

预编译:在预编译的过程中,浏览器会寻找全局变量声明,把它作为window的属性加入到window对象中,并给变量赋值为’undefined’;寻找全局函数声明,把它作为window的方法加入到window对象中,并将函数体赋值给他(匿名函数是不参与预编译的,因为它是变量)。而变量提升作为不合理的地方在ES6中已经解决了,函数提升还存在。

解释执行:执行到变量就赋值,如果变量没有被定义,也就没有被预编译直接赋值,在ES5非严格模式下这个变量会成为window的一个属性,也就是成为全局变量。

4.浏览器重绘与重排的区别?

  • 浏览器中的重绘(Repaint)和重排(Reflow)是页面渲染过程中的两个重要概念,它们虽然有联系,但是涉及的内容和影响范围不同:

    1. 重排(Reflow)

      • 重排是指当页面中的一部分或全部需要重新布局时触发的过程。
      • 重排可能由于以下原因而发生:DOM结构变化、CSS样式变化(如尺寸、位置、显示/隐藏元素)、窗口尺寸变化等。
      • 重排会影响到渲染树(Render Tree)的结构,导致浏览器重新计算元素的几何属性(位置和尺寸),然后重新布局整个页面。
    2. 重绘(Repaint)

      • 重绘是指当页面元素的外观需要更新,但不影响其布局时触发的过程。
      • 重绘可能由于以下原因而发生:CSS样式变化(如颜色、背景、边框等)、页面内容变化(如文本内容更新)等。
      • 重绘不会导致页面的重新布局,浏览器会重新绘制受影响的元素,但不会改变其位置和尺寸。

    因此,重排涉及到整个页面布局的重新计算和重新排列,性能开销相对较大,可能导致页面的闪烁和卡顿;而重绘则仅涉及到外观的更新,性能开销相对较小,但也会影响页面的渲染速度。

7.如何避免重绘或者重排?

  1. 使用 will-change 属性will-change 属性可以告诉浏览器某个元素可能会发生的变化,从而使浏览器提前做好优化准备。例如,设置 will-change: transform; 可以告诉浏览器该元素可能会进行变形,浏览器在处理时会尽量减少重排和重绘。
  2. 使用 translateZ(0) 触发 GPU 加速:将元素的 transform 属性设置为 translateZ(0) 可以触发 GPU 加速,这样可以将动画的计算和渲染交给 GPU 处理,从而减少 CPU 的负载,避免了页面的重排和重绘。
  3. 批量操作 DOM:尽量减少直接操作 DOM,可以将多个 DOM 操作合并成一次操作,或者使用 DocumentFragment 进行批量操作,减少了页面重排和重绘的次数。
  4. 为动画的 HTML 元件**使用 fixedabsoultposition**,那么修改他们的 CSS 是不会 reflow 的。
  5. 不使用 table 布局。因为可能很小的一个小改动会造成整个 table 的重新布局。

8.浏览器的缓存机制

  • 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识
  • 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中

浏览器与服务器通信的方式为应答模式,即是:浏览器发起HTTP请求 – 服务器响应该请求。那么浏览器第一次向服务器发起该请求后拿到请求结果,会根据响应报文中HTTP头的缓存标识,决定是否缓存结果,是则将请求结果和缓存标识存入浏览器缓存中,简单的过程如下图:

由上图我们可以知道:

  • 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识
  • 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中

以上两点结论就是浏览器缓存机制的关键,他确保了每个请求的缓存存入与读取,只要我们再理解浏览器缓存的使用规则,那么所有的问题就迎刃而解了。为了方便理解,这里根据是否需要向服务器重新发起HTTP请求将缓存过程分为两个部分,分别是强制缓存协商缓存

  • 强制缓存

    强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程。当浏览器向服务器发起请求时,服务器会将缓存规则放入HTTP响应报文的HTTP头中和请求结果一起返回给浏览器,控制强制缓存的字段分别是 ExpiresCache-Control,其中Cache-Control优先级比Expires高。

    强制缓存的情况主要有三种(暂不分析协商缓存过程),如下:

    1. 不存在该缓存结果和缓存标识,强制缓存失效,则直接向服务器发起请求(跟第一次发起请求一致)。
    2. 存在该缓存结果和缓存标识,但该结果已失效,强制缓存失效,则使用协商缓存。
    3. 存在该缓存结果和缓存标识,且该结果尚未失效,强制缓存生效,直接返回该结果
  • 协商缓存

    协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,同样,协商缓存的标识也是在响应报文的HTTP头中和请求结果一起返回给浏览器的,控制协商缓存的字段分别有:Last-Modified / If-Modified-SinceEtag / If-None-Match,其中Etag / If-None-Match的优先级比Last-Modified / If-Modified-Since高。协商缓存主要有以下两种情况:

    1. 协商缓存生效,返回304
    2. 协商缓存失效,返回200和请求结果结果

9.进程、线程和协程

进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。

线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。

协程,是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序员自己写程序来管理的轻量级线程叫做『用户空间线程』,具有对内核来说不可见的特性。

10.路由的hash和history模式的区别

hash模式 开发中默认的模式,地址栏URL后携带#,后面为路由。 原理是通过onhashchange()事件监听hash值变化,在页面hash值发生变化后,window就可以监听到事件改变,并按照规则加载相应的代码。hash值变化对应的URL都会被记录下来,这样就能实现浏览器历史页面前进后退。

history模式 history模式中URL没有#,这样相对hash模式更好看,但是需要后台配置支持。

history原理是使用HTML5 history提供的pushStatereplaceState两个API,用于浏览器记录历史浏览栈,并且在修改URL时不会触发页面刷新和后台数据请求。

HTTP

1.http 和 https 的区别及优缺点

  • HTTP 是超文本传输协议,信息是明文传输,HTTPS 协议要比 HTTP协议安全,HTTPS是具有安全性的 SSL加密传输协议,可防止数据在传输过程中被窃取、改变,确保数据的完整性
  • http 协议的默认端口为 80,https 的默认端口为 443
  • http 的连接很简单,是无状态的。https 握手阶段比较费时,会使页面加载时间延长 50%,增加 10%~20%的耗电。
  • https 缓存不如 http 高效,会增加数据开销
  • Https 协议需要 ca 证书,费用较高,功能越强大的证书费用越高

2.https 协议的工作原理

  • 客户端使用 https url 访问服务器,则要求 web 服务器建立 ssl 链接
  • web 服务器接收到客户端的请求之后,会将网站的证书(证书中包含了公钥),传输给客户端
  • 客户端和 web 服务器端开始协商 SSL 链接的安全等级,也就是加密等级。
  • 客户端浏览器通过双方协商一致的安全等级,建立会话密钥,然后通过网站的公钥来加密会话密钥,并传送给网站。
  • web 服务器通过自己的私钥解密出会话密钥
  • web 服务器通过会话密钥加密与客户端之间的通信

3.TCP/IP / 如何保证数据包传输的有序可靠?

对字节流分段并进行编号然后通过 ACK 回复超时重发这两个机制来保证。

  1. 为了保证数据包的可靠传递,发送方必须把已发送的数据包保留在缓冲区;
  2. 并为每个已发送的数据包启动一个超时定时器;
  3. 如在定时器超时之前收到了对方发来的应答信息(可能是对本包的应答,也可以是对本包后续包的应答),则释放该数据包占用的缓冲区;
  4. 否则,重传该数据包,直到收到应答或重传次数超过规定的最大次数为止。
  5. 接收方收到数据包后,先进行CRC校验,如果正确则把数据交给上层协议,然后给发送方发送一个累计应答包,表明该数据已收到,如果接收方正好也有数据要发给发送方,应答包也可方在数据包中捎带过去。

4.TCP和UDP的区别

  • TCP是面向链接的,而UDP是面向无连接的。
  • TCP仅支持单播传输,UDP 提供了单播,多播,广播的功能。
  • TCP的三次握手保证了连接的可靠性; UDP是无连接的、不可靠的一种数据传输协议,首先不可靠性体现在无连接上,通信都不需要建立连接,对接收到的数据也不发送确认信号,发送端不知道数据是否会正确接收。
  • UDP的头部开销比TCP的更小,数据传输速率更高实时性更好

5.HTTP 请求跨域问题

跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的。

同源策略,是浏览器实施的安全限制,只要协议、域名、端口有任何一个不同,都被当作是不同的域。

「同源策略」是一个重要的安全策略,它用于限制一个的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

解决方案

1.JSONP

2.CORS

cors跨域的前提条件是服务器是自己人

在 cors 中会有 简单请求复杂请求的概念

情况一: 使用以下方法(意思就是以下请求以外的都是非简单请求)

情况二: 人为设置以下集合外的请求头

情况三:Content-Type的值仅限于下列三者之一:(例如 application/json 为非简单请求)

  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded

a.简单请求

不会触发 CORS 预检请求。这样的请求为“简单请求”

b.预检请求

预检请求就是先要进行发送一次预检请求到服务器,服务器确定ok后才能像简单请求一样跨域

这个预检请求是以OPTIONS方法发送的

问题1:不可以,需要服务器支持

问题2:不可以,需要抖音服务器支持

问题3:很有可能,因为上传图片使用的是post请求方式,Content-Type为multipart/form-data,但提交表单可能是ajax请求,并且Content-Type为json

1.web 请求设置withCredentials

这里默认情况下在跨域请求,浏览器是不带 cookie 的。但是我们可以通过设置 withCredentials 来进行传递 cookie.

1
2
3
4
5
6
// 原生 xml 的设置方式
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

// axios的 设置方式
axios.defaults.withCredentials = true;

2.设置Access-Control-Allow-Credentialstrue

3.Access-Control-Allow-Origin不能为 *

4.proxy代理服务器

5.postMessage

举例:

6.Cookie、sessionStorage、localStorage 的区别

相同点

  • 存储在客户端

不同点

  • cookie数据大小不能超过4k;sessionStorage和localStorage的存储比cookie大得多,可以达到5M+
  • cookie设置的过期时间之前一直有效;localStorage永久存储,浏览器关闭后数据不丢失除非主动删除数据;sessionStorage数据在当前浏览器窗口关闭后自动删除
  • cookie的数据会自动的传递到服务器;sessionStorage和localStorage数据保存在本地

7.拆包/粘包 问题

拆包

  • 待发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包

粘包

  • 待发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据包合并为一次发送,将发生粘包

  • 接收端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

解决 粘包/拆包 问题

(1)对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;

(2)对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象;

(3)由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。分包多发

以上提到的三种措施,都有其不足之处。

(1)第一种编程设置方法虽然可以避免发送方引起的粘包,但它关闭了优化算法,降低了网络发送效率,影响应用程序的性能,一般不建议使用。

(2)第二种方法只能减少出现粘包的可能性,但并不能完全避免粘包,当发送频率较高时,或由于网络突发可能使某个时间段数据包到达接收方较快,接收方还是有可能来不及接收,从而导致粘包。

(3)第三种方法虽然避免了粘包,但应用程序的效率较低,对实时应用的场合不适合。

好的解决方法:

一种比较周全的对策是:接收方创建一预处理线程,对接收到的数据包进行预处理,将粘连的包分开。实验证明这种方法是高效可行的。

为什么TCP有粘包?

TCP协议粘包拆包问题是因为TCP协议数据传输是基于 “字节流” 的,它不包含消息、数据包等概念,需要应用层协议自己设计消息边界。日常网络应用开发大都在传输层进行,因此粘包拆包问题大都只发生在TCP协议中。

为什么UDP没有粘包?

UDP有消息保护边界,不会发生粘包拆包问题

面经

1.自我介绍

2.vue中的指令有哪些

  • v-text
  • v-html(不建议使用)
  • v-show
  • v-if / v-else-if / v-else
  • v-for
  • v-bind
  • v-on
  • v-model
  • v-slot
  • v-pre(使用频率很低)
  • v-once(使用频率很低)
  • v-cloak(使用频率极低,不细介绍)

3.事件修饰符

.stop - 调用 event.stopPropagation(),禁止事件冒泡

.prevent - 调用 event.preventDefault(),禁止事件的默认行为:

.passive - 立即执行事件的默认行为,会导致 event.preventDefault() 无效:

.capture - 内部元素触发的事件先在此处理,然后才交由内部元素进行处理

.self - 只当事件是从侦听器绑定的元素本身(event.target)触发时才触发回调:

.native - 监听组件根元素的原生事件

.once - 只触发一次回调

4.setup用处

1.基本使用

使用响应式 API 来声明响应式的状态,在 setup() 函数中返回的对象会暴露给模板和组件实例。其它的选项也可以通过组件实例来获取 setup() 暴露的属性

在模板中访问从 setup 返回的 ref 时,它会自动浅层解包,因此你无须再在模板中为它写 .value。当通过 this 访问时也会同样如此解包。

setup() 自身并不含对组件实例的访问权,即在 setup() 中访问 this 会是 undefined。你可以在选项式 API 中访问组合式 API 暴露的值,但反过来则不行。

2.访问 Prop

setup 函数的第一个参数是组件的 props。和标准的组件一致,一个 setup 函数的 props 是响应式的,并且会在传入新的 props 时同步更新。

注意如果你解构了 props 对象,解构出的变量将会丢失响应性。

推荐通过 props.xxx 的形式来使用其中的 props。

如果确实需要解构 props 对象,或者需要将某个 prop 传到一个外部函数中并保持响应性,那么你可以使用 toRefs()toRef() 这两个工具函数:

3.Setup的上下文

传入 setup 函数的第二个参数是一个 Setup 上下文对象。上下文对象暴露了其他一些在 setup 中可能会用到的值:

该上下文对象是非响应式的,可以安全地解构:

attrs 和 slots 都是有状态的对象,它们总是会随着组件自身的更新而更新。这意味着你应当避免解构它们,并始终通过 attrs.x 或 slots.x 的形式使用其中的属性。此外还需注意,和 props 不同,attrs 和 slots 的属性都不是响应式的。如果你想要基于 attrs 或 slots 的改变来执行副作用,那么你应该在 onBeforeUpdate 生命周期钩子中编写相关逻辑。

expose 函数用于显式地限制该组件暴露出的属性,当父组件通过模板引用访问该组件的实例时,将仅能访问 expose 函数暴露出的内容

在父组件通过ref获取子组件的实例的属性和方法的需求中,需要注意:

1.如果子组件是 选项式API组件,基本不需要做任何操作

2.如果子组件是 组合式API组件,需要通过 context.expose 暴露给父组件需要使用的属性和方法

3.如果父组件使用 选项式API, 可以通过 this.$refs.refName 访问到子组件想要你看到的属性和方法

4.如果父组件使用 组合式API,需要在setup中先创建 refName,然后再访问子组件想要你看到的属性和方法(const refName = ref() refName.value.X)

4.与渲染函数一起使用

setup 也可以返回一个渲染函数,此时在渲染函数中可以直接使用在同一作用域下声明的响应式状态:

返回一个渲染函数将会阻止我们返回其他东西。对于组件内部来说,这样没有问题,但如果我们想通过模板引用将这个组件的方法暴露给父组件,那就有问题

我们可以通过调用 expose() 解决这个问题:

5.vite和webpack区别

1、开发模式的差异

在开发环境中,Webpack 是先打包再启动开发服务器,而 Vite 则是直接启动,然后再按需编译依赖文件。(大家可以启动项目后检查源码 Sources 那里看到)

这意味着,当使用 Webpack 时,所有的模块都需要在开发前进行打包,这会增加启动时间和构建时间。

Vite 则采用了不同的策略,它会在请求模块时再进行实时编译,这种按需动态编译的模式极大地缩短了编译时间,特别是在大型项目中,文件数量众多,Vite 的优势更为明显。

Webpack启动

Vite启动

2、对ES Modules的支持

现代浏览器本身就支持 ES Modules,会主动发起请求去获取所需文件。Vite充分利用了这一点,将开发环境下的模块文件直接作为浏览器要执行的文件,而不是像 Webpack 那样先打包,再交给浏览器执行。这种方式减少了中间环节,提高了效率。

什么是ES Modules?

通过使用 exportimport 语句,ES Modules 允许在浏览器端导入和导出模块。

当使用 ES Modules 进行开发时,开发者实际上是在构建一个依赖关系图,不同依赖项之间通过导入语句进行关联。

主流浏览器(除IE外)均支持ES Modules,并且可以通过在 script 标签中设置 type="module"来加载模块。默认情况下,模块会延迟加载,执行时机在文档解析之后,触发DOMContentLoaded事件前。

3、底层语言的差异

Webpack 是基于 Node.js 构建的,而 Vite 则是基于 esbuild 进行预构建依赖。esbuild 是采用 Go 语言编写的,Go 语言是纳秒级别的,而 Node.js 是毫秒级别的。因此,Vite 在打包速度上相比Webpack 有 10-100 倍的提升。

什么是预构建依赖?

预构建依赖通常指的是在项目启动或构建之前,对项目中所需的依赖项进行预先的处理或构建。这样做的好处在于,当项目实际运行时,可以直接使用这些已经预构建好的依赖,而无需再进行实时的编译或构建,从而提高了应用程序的运行速度和效率。

4、热更新的处理

在 Webpack 中,当一个模块或其依赖的模块内容改变时,需要重新编译这些模块。

而在 Vite 中,当某个模块内容改变时,只需要让浏览器重新请求该模块即可,这大大减少了热更新的时间。

总结

总的来说,Vite 之所以比 Webpack 快,主要是因为它采用了不同的开发模式充分利用了现代浏览器的 ES Modules 支持使用了更高效的底层语言并优化了热更新的处理

6.vite的特点和优点

  1. 快速的冷启动:Vite 利用现代浏览器原生 ES 模块的特性,将开发服务器的冷启动时间降至极低,从而实现几乎无感知的开发体验。
  2. 即时热更新:Vite 支持即时热更新,可以在你修改代码后立即看到结果,无需手动刷新浏览器。
  3. 按需编译:Vite 会根据需要按需编译代码,而不是像传统的打包工具那样一次性编译整个项目。这样可以加快开发过程中的构建速度。
  4. 模块预构建:Vite 会在浏览器首次请求时提前预构建模块,以便在浏览器中快速加载。这样可以避免了大部分传统打包工具在构建时需要等待的时间。
  5. 开箱即用的 TypeScript 支持:Vite 内置了对 TypeScript 的支持,无需额外配置即可在项目中使用 TypeScript。

7.vue2的生命周期

Vue 2 的生命周期钩子函数提供了在组件生命周期中不同阶段执行代码的机会。以下是 Vue 2 中常用的生命周期钩子函数及其作用:

  1. beforeCreate:在实例初始化之后,数据观测 (data observation) 和事件配置 (event/watcher setup) 之前被调用。在这个阶段,实例的属性和方法都尚未被初始化。

  2. created:在实例创建完成后被立即调用。在这个阶段,实例已经完成了数据观测 (data observation),属性和方法的运算,但是尚未挂载到 DOM 上。

  3. beforeMount:在挂载开始之前被调用:相关的 render 函数首次被调用。

  4. mounted:el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。在这个阶段,组件已经挂载到 DOM 上,可以进行 DOM 操作。

  5. beforeUpdate:数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。在这个阶段,你可以在更新之前访问现有的 DOM。

  6. updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁后会调用该钩子。在这个阶段,组件已经更新了 DOM,可以执行一些需要 DOM 的操作。

  7. beforeDestroy:实例销毁之前调用。在这个阶段,实例仍然完全可用。

  8. destroyed:实例销毁之后调用。在这个阶段,Vue 实例及其所有的指令已被销毁,但是 DOM 元素仍然存在。

以上是 Vue 2 中常用的生命周期钩子函数。这些钩子函数可以帮助你在组件的不同生命周期阶段执行代码,从而实现更灵活的组件逻辑。

8.双向绑定的理解

双向数据绑定通常是指我们使用的v-model指令的实现,是Vue的一个特性,也可以说是一个input事件和value的语法糖。 Vue通过v-model指令为组件添加上input事件处理和value属性的赋值。

1
2
3
4
<template>
<input v-model='localValue'/>
</template>

上述的组件就相当于如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<!-- 这里添加了input时间的监听和value的属性绑定 -->
<input @input='onInput' :value='localValue' />
<span>{{localValue}}</span>
</template>
<script>
export default{
data(){
return {
localValue:'',
}
},
methods:{
onInput(v){
//在input事件的处理函数中更新value的绑定值
this.localValue=v.target.value;
console.log(this.localValue)
}
}
}
</script>

因此当我们修改input输入框中的值时,我们通过v-model绑定的值也会同步修改,基于上述原理,我们可以很容易的实现一个数据双向绑定的组件。

v-model 本质上不过是语法糖,它负责监听用户的输入事件以更新数据,在大部分情况下, v-model=”foo” 等价于 :value=”foo” 加上 @input=”foo = $event”;
​副作用
​副作用如下:如果 v-model 绑定的是响应式对象上某个不存在的属性,那么 vue 会悄悄地增加这个属性,并让它响应式。
​是单向数据流

​什么是单向数据流?
​子组件不能改变父组件传递给它的 prop 属性,推荐的做法是它抛出事件,通知父组件自行改变绑定的值。
​『单向数据流』总结起来其实也就8个字:『数据向下,事件向上』。


​怎么让你的组件支持v-model

在定义 vue 组件时,你可以提供一个 model 属性,用来定义该组件以何种方式支持 v-model

model 属性本身是有默认值的,如下:

1
2
3
4
5
6
7
8
// 默认的 model 属性
export default {
model: {
prop: 'value',
event: 'input'
}
}

也就是说,如果你不定义 model 属性,或者你按照当面方法定义属性,当其他人使用你的自定义组件时,v-model="foo" 就完全等价于 :value="foo" 加上 @input="foo = $event"

如果把 model 属性进行一些改装,如下:

1
2
3
4
5
6
7
8
// 默认的 model 属性
export default {
model: {
prop: 'ame',
event: 'zard'
}
}

那么,v-model="foo" 就等价于 :ame="foo" 加上 @zard="foo = $event"

没错,就是这么容易,让我们看个例子。

先定义一个自定义组件:

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
<template>
<div>
我们是TI{{ ame }}冠军
<el-button @click="playDota2(1)">加</el-button>
<el-button @click="playDota2(-1)">减</el-button>
</div>
</template>
<script>
export default {
props: {
ame: {
type: Number,
default: 8
}
},
model: { // 自定义v-model的格式
prop: 'ame', // 代表 v-model 绑定的prop名
event: 'zard' // 代码 v-model 通知父组件更新属性的事件名
},
methods: {
playDota2(step) {
const newYear = this.ame + step
this.$emit('zard', newYear)
}
}
}
</script>

然后我们在父组件中使用该组件:

1
2
3
4
5
6
7
8
9
10
11
// template中
<dota v-model="ti"></dota>
// script中
export default {
data() {
return {
ti: 8
}
}
}

看看效果:

9.var const 和 let的区别

varletconst三者区别可以围绕下面五点展开:

  • 变量提升
  • 暂时性死区
  • 块级作用域
  • 重复声明
  • 修改声明的变量
  • 使用

变量提升

1
var`声明的变量存在变量提升,即变量可以在声明之前调用,值为`undefined

letconst不存在变量提升,即它们所声明的变量一定要在声明后使用,否则报错

1
2
3
4
5
6
7
8
9
10
11
// var
console.log(a) // undefined
var a = 10

// let
console.log(b) // Cannot access 'b' before initialization
let b = 10

// const
console.log(c) // Cannot access 'c' before initialization
const c = 10

暂时性死区

var不存在暂时性死区

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

1
2
3
4
5
6
7
8
9
10
11
// var
console.log(a) // undefined
var a = 10

// let
console.log(b) // Cannot access 'b' before initialization
let b = 10

// const
console.log(c) // Cannot access 'c' before initialization
const c = 10

块级作用域

var不存在块级作用域

letconst存在块级作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// var
{
var a = 20
}
console.log(a) // 20

// let
{
let b = 20
}
console.log(b) // Uncaught ReferenceError: b is not defined

// const
{
const c = 20
}
console.log(c) // Uncaught ReferenceError: c is not defined

重复声明

var允许重复声明变量

letconst在同一作用域不允许重复声明变量

1
2
3
4
5
6
7
8
9
10
11
// var
var a = 10
var a = 20 // 20

// let
let b = 10
let b = 20 // Identifier 'b' has already been declared

// const
const c = 10
const c = 20 // Identifier 'c' has already been declared

修改声明的变量

varlet可以

const声明一个只读的常量。一旦声明,常量的值就不能改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// var
var a = 10
a = 20
console.log(a) // 20

//let
let b = 10
b = 20
console.log(b) // 20

// const
const c = 10
c = 20
console.log(c) // Uncaught TypeError: Assignment to constant variable

使用

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

10.组件结构的两种组织方式

11.语义化结构开发的方式

12.H5语义化标签有哪些

header、nav、article、section、aside、footer

13.js数据结构开发指什么

14.组件之间的通信方式

vue2父子通信方式

1.props 父传子

2..sync的使用
​可以帮我们实现父组件向子组件传递的数据 的双向绑定,所以子组件接收到数据后可以直接修改,并且会同时修改父组件的数据

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
// Parent.vue
<template>
<child :page.sync="page"></child>
</template>
<script>
export default {
data(){
return {
page:1
}
}
}

// Child.vue
export default {
props:["page"],
computed(){
// 当我们在子组件里修改 currentPage 时,父组件的 page 也会随之改变
currentPage {
get(){
return this.page
},
set(newVal){
this.$emit("update:page", newVal)
}
}
}
}
</script>

3.v-model
​v-model 本质上不过是语法糖,它负责监听用户的输入事件以更新数据,在大部分情况下, v-model=”foo” 等价于 :value=”foo” 加上 @input=”foo = $event”;
​副作用
​副作用如下:如果 v-model 绑定的是响应式对象上某个不存在的属性,那么 vue 会悄悄地增加这个属性,并让它响应式。
​是单向数据流

​什么是单向数据流?
​子组件不能改变父组件传递给它的 prop 属性,推荐的做法是它抛出事件,通知父组件自行改变绑定的值。
​『单向数据流』总结起来其实也就8个字:『数据向下,事件向上』。


​怎么让你的组件支持v-model

在定义 vue 组件时,你可以提供一个 model 属性,用来定义该组件以何种方式支持 v-model

model 属性本身是有默认值的,如下:

1
2
3
4
5
6
7
8
// 默认的 model 属性
export default {
model: {
prop: 'value',
event: 'input'
}
}

也就是说,如果你不定义 model 属性,或者你按照当面方法定义属性,当其他人使用你的自定义组件时,v-model="foo" 就完全等价于 :value="foo" 加上 @input="foo = $event"

如果把 model 属性进行一些改装,如下:

1
2
3
4
5
6
7
8
// 默认的 model 属性
export default {
model: {
prop: 'ame',
event: 'zard'
}
}

那么,v-model="foo" 就等价于 :ame="foo" 加上 @zard="foo = $event"

没错,就是这么容易,让我们看个例子。

先定义一个自定义组件:

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
<template>
<div>
我们是TI{{ ame }}冠军
<el-button @click="playDota2(1)">加</el-button>
<el-button @click="playDota2(-1)">减</el-button>
</div>
</template>
<script>
export default {
props: {
ame: {
type: Number,
default: 8
}
},
model: { // 自定义v-model的格式
prop: 'ame', // 代表 v-model 绑定的prop名
event: 'zard' // 代码 v-model 通知父组件更新属性的事件名
},
methods: {
playDota2(step) {
const newYear = this.ame + step
this.$emit('zard', newYear)
}
}
}
</script>

然后我们在父组件中使用该组件:

1
2
3
4
5
6
7
8
9
10
11
// template中
<dota v-model="ti"></dota>
// script中
export default {
data() {
return {
ti: 8
}
}
}

看看效果:

​4.模版引用ref
​ref 如果在普通的DOM元素上,引用指向的就是该DOM元素;

​如果在子组件上,引用的指向就是子组件实例,然后父组件就可以通过 ref 主动获取子组件的属性或者调用子组件的方法

​5. $emit / v-on
​子组件通过派发事件的方式给父组件数据,或者触发父组件更新等操作

​6. $attrs / $listeners
​多层嵌套组件传递数据时,如果只是传递数据,而不做中间处理的话就可以用这个,比如父组件向孙子组件传递数据时
$attrs:包含父作用域里除 class 和 style 除外的非 props 属性集合。通过 this.$attrs 获取父作用域中所有符合条件的属性集合,然后还要继续传给子组件内部的其他组件,就可以通过 v-bind=”$attrs”
$listeners:包含父作用域里 .native 除外的监听事件集合。如果还要继续传给子组件内部的其他组件,就可以通过 v-on=”$linteners”
​使用方式是相同的

​7. $children / $parent
$children:获取到一个包含所有子组件(不包含孙子组件)的 VueComponent 对象数组,可以直接拿到子组件中所有数据和方法等
$parent:获取到一个父节点的 VueComponent 对象,同样包含父节点中所有数据和方法等


​8.provide / inject
​provide / inject 为依赖注入,说是不推荐直接用于应用程序代码中,但是在一些插件或组件库里却是被常用,所以我觉得用也没啥,还挺好用的

​provide:可以让我们指定想要提供给后代组件的数据或方法

​inject:在任何后代组件中接收想要添加在这个组件上的数据或方法,不管组件嵌套多深都可以直接拿来用

​要注意的是 provide 和 inject 传递的数据不是响应式的,也就是说用 inject 接收来数据后,provide 里的数据改变了,后代组件中的数据不会改变,除非传入的就是一个可监听的对象

​所以建议还是传递一些常量或者方法

​9.EventBus
​EventBus 是中央事件总线,不管是父子组件,兄弟组件,跨层级组件等都可以使用它完成通信操作

​10.Vuex
​Vuex 是状态管理器,集中式存储管理所有组件的状态。

​11. $root
$root 可以拿到 App.vue 里的数据和方法

​12. slot
​就是把子组件的数据通过插槽的方式传给父组件使用,然后再插回来


​Vue3 组件通信方式

  • props

  • $emit

  • expose / ref

  • $attrs

  • v-model

  • provide / inject

  • Vuex

  • mitt

    • Vue3 中没有了 EventBus 跨组件通信,但是现在有了一个替代的方案 mitt.js,原理还是 EventBus
      先安装 npm i mitt -S
      然后像以前封装 bus 一样,封装一下

      1
      2
      3
      4
      //mitt.js
      import mitt from 'mitt'
      const mitt = mitt()
      export default mitt

15.vuex状态管理包含哪些部分

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
    state: { 
// state 类似 data
//这里面写入数据
},
getters:{
// getters 类似 computed
// 在这里面写个方法
},
mutations:{
// mutations 类似 methods
// 写方法对数据做出更改(同步操作)
},
actions:{
// actions 类似 methods
// 写方法对数据做出更改(异步操作)
}
module:{

//modules,可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理;如果所有的状态或者方法都写在一个`store`里面,将会变得非常臃肿,难以维护。
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}

}

16.vuex的语法糖有哪些

Vuex 提供了一些语法糖,以简化在 Vue 应用中使用 Vuex 的过程。以下是一些常用的 Vuex 语法糖:

  1. mapState:将 store 中的状态映射到组件的计算属性中,以便在模板中直接使用。例如:

    1
    2
    3
    4
    5
    6
    7
    import { mapState } from 'vuex';

    export default {
    computed: {
    ...mapState(['count'])
    }
    }

    在模板中可以直接使用 {{ count }} 来访问 store.state.count 的值。

  2. mapGetters:将 store 中的 getters 映射到组件的计算属性中,以便在模板中直接使用。例如:

    1
    2
    3
    4
    5
    6
    7
    import { mapGetters } from 'vuex';

    export default {
    computed: {
    ...mapGetters(['doneTodosCount'])
    }
    }

    在模板中可以直接使用 {{ doneTodosCount }} 来访问 store.getters.doneTodosCount 的值。

  3. mapMutations:将 store 中的 mutations 映射到组件的 methods 中,以便在方法中直接调用。例如:

    1
    2
    3
    4
    5
    6
    7
    import { mapMutations } from 'vuex';

    export default {
    methods: {
    ...mapMutations(['increment'])
    }
    }

    在方法中可以直接调用 this.increment() 来提交 store.commit('increment')

  4. mapActions:将 store 中的 actions 映射到组件的 methods 中,以便在方法中直接调用。例如:

    1
    2
    3
    4
    5
    6
    7
    import { mapActions } from 'vuex';

    export default {
    methods: {
    ...mapActions(['incrementAsync'])
    }
    }

    在方法中可以直接调用 this.incrementAsync() 来分发 store.dispatch('incrementAsync')

这些语法糖可以使 Vuex 的使用更加简洁和方便,减少了模板和方法中的冗余代码,提高了代码的可读性和可维护性。

17.ajax、fetch、axios的区别

1.ajax

英译过来是Aysnchronous JavaScript And XML,直译是异步JSXMLXML类似HTML,但是设计宗旨就为了传输数据,现已被JSON代替),解释一下就是说XML作为数据传输格式发送JS异步请求。但实际上ajax是一个一类技术的统称的术语,包括XMLHttpRequestJSCSSDOM等,它主要实现网页拿到请求数据后不用刷新整个页面也能呈现最新的数据

下面我们简单封装一个ajax请求【面试高频题】:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
javascript复制代码const ajaxGet = function (url) {
const xhr = new XMLHttpRequest()
xhr.open('get', url)
xhr.onreadystatechange = () => {
if (xhr.readyState == 4) {
if (xhr.status >= 200 && xhr.status < 400) {
console.log(xhr.response); // 响应结果
}
}
}
xhr.onerror = (error) => {
console.log(error, xhr.status)
}
xhr.send()
}

2. fetch

它其实就是一个JS自带的发送请求的一个api,拿来跟ajax对比是完全不合理的,它们完全不是一个概念的东西,适合拿来和fetch对比的其实是xhr,也就是上面封装ajax请求的代码里的XMLHttpRequest,这两都是JS自带的发请求的方法,而fetchES6出现的,自然功能比xhr更强,主要原因就是它是基于Promise的,它返回一个Promise,因此可以使用.then(res => )的方式链式处理请求结果,这不仅提高了代码的可读性,还避免了回调地狱(xhr通过xhr.onreadystatechange= () => {}这样回调的方式监控请求状态,要是想在请求后再发送请求就要在回调函数内再发送请求,这样容易出现回调地狱)的问题。而且JS自带,语法也非常简洁,几行代码就能发起一个请求,用起来很方便,据说大佬都爱用。

它的特点是:

  • 使用 promise,不使用回调函数。
  • 采用模块化设计,比如 rep、res 等对象分散开来,比较友好。
  • 通过数据流对象处理数据,可以提高网站性能。

下面我们简单写个fetch请求的示例:

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
javascript复制代码// get请求
fetch('http://127.0.0.1:8000/get')
.then(res => {
if (!res.ok) {
throw new Error('请求错误!状态码为:', res.status)
}
return res.text()
}).then(data => {
console.log(data);
})
// post请求
fetch('http://127.0.0.1:8000/post', {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
mode: 'no-cors', // 设置cors表示只能发送跨域的请求,no-cors表示跨不跨域都能发
body: JSON.stringify({
name: 'zhangsan',
age: 18
})
}).then(res => {
return res.json()
}).then(data => {
console.log(data);
})

3. axios

axios是用于网络请求的第三方库,它是一个库。axios利用xhr进行了二次封装的请求库,xhr只是axios中的其中一个请求适配器,axios在nodejs端还有个http的请求适配器;axios = xhr + http;它返回一个Promise。【项目中经常需要封装的axios】

它的特点:

  • 在浏览器环境中创建 XMLHttpRequests;在node.js环境创建 http 请求
  • 返回Promise
  • 拦截请求和响应
  • 自动转换 JSON 数据
  • 转换请求数据和响应数据
  • 取消请求

它的基础语法是:

1
2
3
4
5
6
7
8
9
10
11
12
javascript复制代码// 发送 Get 请求
axios({
method: 'get',
url: '',
params: {} // 查询query使用params
})
// 发送 Post 请求
axios({
method: 'post',
url: '',
data: {} // 请求体body用data
})

下面我们在vue项目中封装一个使用axios实现的请求。

libs/config.js:配置文件

1
2
3
4
5
javascript复制代码const serverConfig = {
baseUrl: "http://127.0.0.1:8000", // 请求基础地址,可根据环境自定义
useTokenAuthentication: false, // 是否开启token认证
};
export default serverConfig;

libs/request.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
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
79
80
81
82
83
84
85
86
87
88
89
90
91
javascript复制代码import axios from "axios";  // 第三方库 需要安装
import serverConfig from "./config";
// 创建axios实例
const apiClient = axios.create({
baseURL: serverConfig.baseUrl, // 基础请求地址
withCredentials: false, // 跨域请求是否需要携带cookie
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
timeout: 10000, // 请求超时时间
});

// 请求拦截
apiClient.interceptors.request.use(
(config) => {
// 请求发送前的处理逻辑 比如token认证,设置各种请求头啥的
// 如果开启token认证
if (serverConfig.useTokenAuthentication) {
// 请求头携带token
config.headers.Authorization = localStorage.getItem("token");
}
return config;
},
(error) => {
// 请求发送失败的处理逻辑
return Promise.reject(error);
}
);

// 响应拦截
apiClient.interceptors.response.use(
(response) => {
// 响应数据处理逻辑,比如判断token是否过期等等
// 代码块
return response;
},
(error) => {
// 响应数据失败的处理逻辑
let message = "";
if (error && error.response) {
switch (error.response.status) {
case 302:
message = "接口重定向了!";
break;
case 400:
message = "参数不正确!";
break;
case 401:
message = "您未登录,或者登录已经超时,请先登录!";
break;
case 403:
message = "您没有权限操作!";
break;
case 404:
message = `请求地址出错: ${error.response.config.url}`;
break;
case 408:
message = "请求超时!";
break;
case 409:
message = "系统已存在相同数据!";
break;
case 500:
message = "服务器内部错误!";
break;
case 501:
message = "服务未实现!";
break;
case 502:
message = "网关错误!";
break;
case 503:
message = "服务不可用!";
break;
case 504:
message = "服务暂时无法访问,请稍后再试!";
break;
case 505:
message = "HTTP 版本不受支持!";
break;
default:
message = "异常问题,请联系管理员!";
break;
}
}
return Promise.reject(message);
}
);

export default apiClient;

/api/index.js:配置请求接口,这里一个get一个post

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
javascript复制代码import apiClient from "@/libs/request";

let getInfo = (params) => {
return apiClient({
url: "/get",
method: "get",
params, // axios的get请求query用params
});
};
let postInfo = (params) => {
return apiClient({
url: "/post",
method: "post",
data: params, // axios的post请求body用data
});
};
export default {
getInfo,
postInfo,
};

App.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
javascript复制代码<script>
import api from './api/index.js'
export default {
data() {
return {
isH5: true
}
},
created() {
this.init()
},
methods: {
init() {
api.getInfo().then(res => {
console.log(res.data);
})
api.postInfo({
name: 'zhangsan',
age: '18'
}).then(res => {
console.log(res.data);
})
}
},
}
</script>

18.promise有几种状态

  1. Pending(待定):Promise 对象刚被创建时的初始状态,表示还没有被执行或者被拒绝。

  2. Fulfilled(已完成):表示操作成功完成,Promise 对象的状态从 Pending 变为 Fulfilled,意味着异步操作已经成功完成,且相应的结果已经被返回。

  3. Rejected(已拒绝):表示操作失败,Promise 对象的状态从 Pending 变为 Rejected,意味着异步操作失败或者出错,且相应的错误信息被返回。

19.promise状态有几个过程

Promise 对象会从 Pending 状态转换为 Fulfilled 或 Rejected 状态,一旦转换完成,Promise 的状态就不会再发生变化。此外,Promise 的状态一旦发生变化,就会立即执行相应的状态处理函数,即 then 方法的回调函数或 catch 方法的回调函数。

20.组件结构包含哪几个部分

在 Vue 中,一个组件通常由三个部分组成:

  1. 模板(Template):模板定义了组件的结构,以及组件中所需的 HTML 结构和 Vue 模板语法。模板是组件的可视化部分,决定了组件在页面上的呈现方式。
  2. 脚本(Script):脚本定义了组件的行为和逻辑。在脚本中,你可以定义组件的数据、计算属性、方法、生命周期钩子等。通常,脚本是使用 JavaScript 或 TypeScript 编写的。
  3. 样式(Style):样式定义了组件的外观和样式。你可以使用 CSS、Sass、Less 等方式来编写组件的样式。Vue 支持在单文件组件中使用 <style> 标签来定义组件的样式,也支持 CSS 模块化和预处理器等特性。

21.怎么接收传递过来的方法

  • 父组件 Parent.vue 通过 props 将一个名为 callback 的方法传递给子组件 Child.vue。
  • 子组件 Child.vue 使用 props 来声明接收父组件传递过来的方法,并在需要的时候调用该方法。

通过这种方式,子组件就可以与父组件进行通信,从而实现了方法的传递和调用。

22.JS数据的基本类型有哪些?

JavaScript 的基本数据类型包括:

  1. 字符串(String)
  2. 数字(Number)
  3. 布尔值(Boolean)
  4. 空值(Null)
  5. 未定义(Undefined)
  6. 符号(Symbol)
  7. BigInt(BigInt)

23.通过中间件实现文件上传,文件上传是怎么实现的

24.用input来实现,怎么实现

使用 <input type="file"> 元素可以实现简单的文件上传功能。下面是一个基本的示例,演示了如何通过 <input> 元素来实现文件上传:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Upload Example</title>
</head>
<body>
<h1>File Upload Example</h1>
<form id="uploadForm" enctype="multipart/form-data">
<input type="file" name="file" id="fileInput">
<button type="submit">Upload</button>
</form>

<div id="result"></div>

<script>
const form = document.getElementById('uploadForm');
const fileInput = document.getElementById('fileInput');
const resultDiv = document.getElementById('result');

form.addEventListener('submit', async (e) => {
e.preventDefault();

const formData = new FormData();
formData.append('file', fileInput.files[0]);

try {
const response = await fetch('/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
resultDiv.textContent = data.message;
} catch (error) {
console.error('Error uploading file:', error);
resultDiv.textContent = 'Error uploading file.';
}
});
</script>
</body>
</html>

在上面的示例中:

  • 我们使用了 <input type="file"> 元素来创建文件上传的表单字段。
  • 用户可以点击该元素来选择要上传的文件。
  • 我们监听了表单的 submit 事件,阻止了默认的表单提交行为。
  • 在提交事件处理程序中,我们使用 FormData 对象来构建一个包含上传文件的表单数据。
  • 然后,我们使用 Fetch API 来发送 POST 请求到服务器的 ‘/upload’ 路径,并将 FormData 对象作为请求体。
  • 最后,我们将服务器返回的消息显示在页面上。

需要注意的是,这只是一个前端的示例,后端的上传处理逻辑需要根据具体的后端框架来实现。

25.什么情况会出现跨域请求

跨域请求(Cross-Origin Request)是指在浏览器中,当当前页面的域名、协议、端口与请求目标不一致时,发起的 HTTP 请求就会被认为是跨域请求。跨域请求可能会导致一些安全性问题,因此浏览器会限制跨域请求的执行。

以下情况会导致跨域请求发生:

  1. 不同域名:最常见的情况是请求的域名不同,例如从 example.com 的页面向 api.example.com 发起请求。

  2. 不同协议:如果当前页面使用了 http://,而请求目标是 https://,也会被视为跨域请求。

  3. 不同端口:即使是同一个域名,但如果端口不同(例如一个在 http://example.com:8080,另一个在 http://example.com:3000),也会被认为是跨域请求。

  4. 不同子域名:不同的子域名之间(如 a.example.comb.example.com)也被认为是不同的域名,会触发跨域请求。

跨域请求会受到同源策略(Same Origin Policy)的限制,同源策略是浏览器为了保护用户信息安全而实施的一种安全机制,它限制了一个页面中的脚本只能与其来源相同的资源进行交互。为了在跨域请求时安全可控,可以通过跨域资源共享(CORS)、JSONP、代理等方式来解决跨域请求的问题。

26.防抖和节流的区别

防抖(Debounce)和节流(Throttle)是用于控制函数执行频率的两种常见技术,它们都可以用于优化性能和提升用户体验,但它们的原理和应用场景略有不同。

  1. 防抖(Debounce)

    • 防抖是指在事件触发后等待一定时间间隔后执行函数,如果在等待时间内又触发了该事件,则重新计时,直到事件停止触发,最后一次执行函数。
    • 防抖适用于需要等待用户停止操作后执行的场景,如输入框输入事件、滚动事件等。
    • 举例:当用户在输入框中连续输入文字时,如果每次输入都触发搜索请求,会造成频繁的请求,影响性能。通过防抖技术,可以等待用户停止输入一段时间后再发起搜索请求,避免频繁请求服务器。
  2. 节流(Throttle)

    • 节流是指在一定时间间隔内只执行一次函数,即使事件在间隔内被触发多次,也只会执行一次函数。
    • 节流适用于需要控制函数执行频率的场景,如页面滚动事件、resize 事件等。
    • 举例:当用户滚动页面时,如果滚动事件处理函数需要执行昂贵的计算或者频繁的 DOM 操作,可以通过节流技术控制滚动事件处理函数的执行频率,减少不必要的计算和操作,提高性能。

总结来说,防抖是为了防止函数在短时间内被频繁调用,而节流是为了控制函数在一定时间间隔内的执行频率。两者在实现上有所不同,但都能有效地优化函数执行的性能。选择使用哪种技术取决于具体的应用场景和需求。

27.定时器了解吗

是的,定时器是一种常见的编程工具,用于在一定的时间间隔后执行特定的代码或者在特定的时间点执行代码。在 JavaScript 中,有两种常见的定时器:setTimeoutsetInterval

  1. setTimeout

    • setTimeout 函数用于在指定的延迟之后执行一次特定的代码。
    • 它接受两个参数:要执行的函数和延迟的毫秒数。
    • 例如,下面的代码会在 1000 毫秒(1 秒)后输出 “Hello, world!”:
      1
      2
      3
      setTimeout(() => {
      console.log('Hello, world!');
      }, 1000);
  2. setInterval

    • setInterval 函数用于在每个指定的时间间隔执行特定的代码,直到被清除。
    • 它接受两个参数:要执行的函数和时间间隔的毫秒数。
    • 例如,下面的代码会每隔 2000 毫秒(2 秒)输出一次 “Hello, world!”:
      1
      2
      3
      setInterval(() => {
      console.log('Hello, world!');
      }, 2000);

需要注意的是,尽管定时器可以实现一些功能,但过度使用定时器可能会导致性能问题或者不可预料的行为,特别是在处理异步操作时。因此,在使用定时器时应谨慎考虑其影响,并确保适当地管理定时器,避免内存泄漏或其他问题。

28.怎么移除定时器

要移除定时器,可以使用 clearTimeoutclearInterval 函数,具体取决于你使用的是 setTimeout 还是 setInterval

  1. 移除 setTimeout

    • 使用 clearTimeout 函数来移除由 setTimeout 创建的定时器。
    • clearTimeout 接受一个参数,即 setTimeout 返回的定时器标识符(ID)。
    • 例如,下面的代码创建了一个 setTimeout 定时器,并在 1000 毫秒后移除它:
      1
      2
      3
      4
      5
      6
      const timerId = setTimeout(() => {
      console.log('Hello, world!');
      }, 1000);

      // 移除定时器
      clearTimeout(timerId);
  2. 移除 setInterval

    • 使用 clearInterval 函数来移除由 setInterval 创建的定时器。
    • clearInterval 接受一个参数,即 setInterval 返回的定时器标识符(ID)。
    • 例如,下面的代码创建了一个 setInterval 定时器,并在 2000 毫秒后移除它:
      1
      2
      3
      4
      5
      6
      const timerId = setInterval(() => {
      console.log('Hello, world!');
      }, 2000);

      // 移除定时器
      clearInterval(timerId);

需要注意的是,传递给 clearTimeoutclearInterval 的定时器标识符必须是由对应的 setTimeoutsetInterval 返回的有效标识符。否则,移除操作将无效。因此,在创建定时器时,应该保存返回的定时器标识符,以便在需要时移除定时器。

29.表单组件的自定义UI,支持渲染多种结构

30.element-ui表单已经写好了一套

31.浏览器请求一个网站,会做什么操作

当浏览器请求一个网站时,通常会执行以下操作:

  1. DNS 解析:浏览器首先会将网址解析成对应的 IP 地址,这个过程称为 DNS 解析。浏览器会首先查询本地缓存中是否有对应的 DNS 记录,如果没有,就会向 DNS 服务器发送请求,获取域名对应的 IP 地址。

  2. 建立 TCP 连接:浏览器通过 IP 地址向目标服务器发起 TCP 连接。在 TCP 握手过程中,客户端和服务器会交换一些信息,确认双方可以通信。

  3. 发送 HTTP 请求:建立 TCP 连接后,浏览器会向服务器发送 HTTP 请求,请求网站的资源。HTTP 请求包括请求行、请求头和请求体等信息,其中请求行包含请求方法(GET、POST 等)、请求的 URL 和协议版本等。

  4. 服务器处理请求:服务器接收到浏览器发送的请求后,会根据请求的 URL 和其他信息,执行相应的处理逻辑。这可能涉及到动态生成内容、查询数据库、调用后端服务等操作。

  5. 返回 HTTP 响应:服务器处理完请求后,会将处理结果封装成 HTTP 响应返回给浏览器。HTTP 响应包括响应行、响应头和响应体等信息,其中响应行包含状态码(例如 200、404 等)和协议版本等。

  6. 接收响应并渲染页面:浏览器接收到服务器返回的 HTTP 响应后,会根据响应的内容进行相应的处理。如果请求的是 HTML 页面,浏览器会解析 HTML、CSS 和 JavaScript,并渲染页面;如果请求的是其他类型的资源(如图片、视频、样式表等),浏览器会根据相应的类型进行处理和展示。

  7. 关闭 TCP 连接:页面加载完成后,浏览器会关闭与服务器的 TCP 连接,释放资源。

这是一个简化的浏览器请求网站的过程,实际过程中可能还涉及到缓存、重定向、安全验证等其他操作。

32.前端缓存有哪几种?

前端缓存主要分为以下几种:

  1. 浏览器缓存

    • 浏览器缓存是指浏览器在本地保存已经访问过的资源副本,以便在下次访问相同资源时能够直接从本地加载,而不必重新请求服务器。
    • 浏览器缓存可以分为强缓存和协商缓存两种方式。强缓存是根据资源的缓存控制头(例如 Cache-Control 和 Expires)判断是否使用本地缓存,而协商缓存则是通过与服务器协商确定是否使用缓存。
  2. Service Worker 缓存

    • Service Worker 是运行在浏览器后台的 JavaScript 脚本,可以拦截和处理网络请求。
    • Service Worker 可以用来实现高级的缓存策略,如离线缓存、动态缓存、预加载等,从而提高应用的性能和可靠性。
  3. Web Storage

    • Web Storage 包括 localStorage 和 sessionStorage 两种 API,用于在浏览器中保存键值对数据。
    • localStorage 数据在浏览器关闭后仍然保留,直到用户手动清除;而 sessionStorage 数据仅在当前会话中有效,关闭标签页或浏览器后即被删除。
  4. IndexedDB

    • IndexedDB 是浏览器提供的一种客户端数据库,用于在浏览器中保存大量结构化数据。
    • IndexedDB 允许开发者在客户端存储和检索数据,并支持事务操作和索引查询,适用于需要离线访问大量数据的应用场景。
  5. Cookie

    • Cookie 是一种在浏览器和服务器之间传递的小型文本文件,用于存储用户的身份认证信息、会话状态等。
    • Cookie 可以设置过期时间,可以在浏览器关闭后仍然保留,也可以在不同的页面之间共享数据。

这些前端缓存技术各有特点,可以根据具体的应用场景选择合适的缓存策略来提高页面性能和用户体验。

33.Localstoreage的有效期是多久

LocalStorage 的数据没有固定的过期时间,它的数据会一直保存在浏览器中,直到用户手动删除或者清除浏览器缓存。因此,LocalStorage 中存储的数据是持久化的,不受会话的影响。

需要注意的是,LocalStorage 存储的数据是以域名为单位进行存储的,不同域名之间的数据是相互隔离的。另外,由于LocalStorage是以字符串的形式存储数据的,因此存储的数据量是受限的,一般为 5MB 到 10MB 左右,不同浏览器的限制可能会有所不同。

34.session的有效期呢

Session 的有效期是在用户关闭浏览器或者长时间不活动后,服务器会自动清除用户的会话数据,从而使会话失效。具体的失效时间取决于服务器的配置和会话管理策略。

一般情况下,Session 的有效期由服务器端管理,可以通过设置会话超时时间来控制。在大多数 Web 服务器和 Web 框架中,会话超时时间可以在配置文件中进行设置,通常以分钟为单位。一旦用户在一段时间内没有与服务器进行交互,会话就会失效,并且用户需要重新进行身份验证或重新登录。

需要注意的是,Session 的失效时间是相对灵活的,它可以根据应用的需求和安全考虑进行调整。一般情况下,Session 的超时时间设置得太长可能会增加安全风险,而设置得太短则可能影响用户体验。因此,合理设置 Session 的有效期是很重要的。

35.cookie知道吗

当然,我了解 Cookie。Cookie 是在客户端(浏览器)和服务器之间传递的小型文本文件,用于存储用户的身份认证信息、会话状态等。Cookie 通常由服务器发送给浏览器,并由浏览器存储在用户的计算机上。

以下是一些关于 Cookie 的重要信息:

  1. 存储位置:Cookie 存储在用户的计算机上,通常保存在浏览器的 Cookie 文件夹中。

  2. 作用域:Cookie 是与特定的域名相关联的,不同域名之间的 Cookie 是相互隔离的,一个域名设置的 Cookie 无法被其他域名访问。

  3. 过期时间:Cookie 可以设置过期时间,超过过期时间后,Cookie 将被自动删除。如果不设置过期时间,那么 Cookie 将成为会话 Cookie,会随着浏览器的关闭而被删除。

  4. 安全属性:可以通过设置 Cookie 的 Secure 属性,使其只能在使用 HTTPS 协议时被发送到服务器,增加了 Cookie 的安全性。

  5. HttpOnly 属性:设置了 HttpOnly 属性的 Cookie 不能通过 JavaScript 脚本来访问,这样可以减少 XSS 攻击的风险。

  6. SameSite 属性:设置 SameSite 属性可以防止跨站点请求伪造(CSRF)攻击,它定义了 Cookie 在跨站点请求中是否可以被发送。

Cookie 在 Web 开发中广泛用于实现用户身份认证、会话管理、跟踪用户行为等功能。然而,由于 Cookie 是存储在用户计算机上的敏感数据,因此在使用 Cookie 时需要注意安全性和隐私保护。

36.css盒子模型

CSS 盒子模型是 CSS 中用于布局的基本概念之一,它描述了在网页中的每个元素都被看作是一个矩形的盒子,并且这个盒子具有内容区域、内边距、边框和外边距四个部分组成。

盒子模型包含以下几个部分:

  1. 内容区域(Content):内容区域包含了元素的实际内容,例如文本、图片等。内容区域的大小由元素的宽度(width)和高度(height)属性来确定。

  2. 内边距(Padding):内边距是内容区域与边框之间的空白区域,用于控制内容与边框之间的间距。内边距可以通过 padding 属性来设置,分为上、右、下、左四个方向的值,也可以使用 padding-toppadding-rightpadding-bottompadding-left 分别设置。

  3. 边框(Border):边框是围绕在内容区域和内边距外部的线条或者区域,用于装饰和分隔元素。边框的样式、宽度和颜色可以通过 border 属性来设置。

  4. 外边距(Margin):外边距是元素与其相邻元素之间的空白区域,用于控制元素与其他元素之间的间距。外边距可以通过 margin 属性来设置,分为上、右、下、左四个方向的值,也可以使用 margin-topmargin-rightmargin-bottommargin-left 分别设置。

盒子模型的宽度和高度计算方式为:内容区域的宽度/高度 + 内边距 + 边框。外边距不会影响盒子的实际大小,而是会影响盒子与其他盒子之间的距离。

在 CSS 中,可以通过设置 box-sizing 属性来控制盒子模型的计算方式,常见的取值有 content-boxborder-boxcontent-box 是默认值,表示盒子的宽度和高度只包括内容区域;border-box 表示盒子的宽度和高度包括内容区域、内边距和边框。

37.正常的盒子模型包含什么

正常的盒子模型包含以下几个部分:

  1. 内容区域(Content):内容区域包含了元素的实际内容,例如文本、图片等。内容区域的大小由元素的宽度(width)和高度(height)属性来确定。

  2. 内边距(Padding):内边距是内容区域与边框之间的空白区域,用于控制内容与边框之间的间距。内边距可以通过 padding 属性来设置,分为上、右、下、左四个方向的值,也可以使用 padding-toppadding-rightpadding-bottompadding-left 分别设置。

  3. 边框(Border):边框是围绕在内容区域和内边距外部的线条或者区域,用于装饰和分隔元素。边框的样式、宽度和颜色可以通过 border 属性来设置。

  4. 外边距(Margin):外边距是元素与其相邻元素之间的空白区域,用于控制元素与其他元素之间的间距。外边距可以通过 margin 属性来设置,分为上、右、下、左四个方向的值,也可以使用 margin-topmargin-rightmargin-bottommargin-left 分别设置。

这些部分共同构成了一个元素的盒子模型,通过设置这些属性可以控制元素的大小、间距、边框样式等,从而实现网页布局和样式设计。

38.垂直居中的方式

实现垂直居中的方式有多种,以下是其中几种常见的方法:

  1. 使用 Flexbox
    使用 Flexbox 是实现垂直居中的最简单和最常用的方法之一。可以通过设置容器的 display: flex;align-items: center; 来使其内部元素垂直居中。

    1
    2
    3
    4
    .container {
    display: flex;
    align-items: center; /* 垂直居中 */
    }
  2. 使用 Grid Layout
    类似于 Flexbox,使用 CSS Grid 也可以实现垂直居中。通过设置容器的 display: grid;align-items: center; 来使其内部元素垂直居中。

    1
    2
    3
    4
    .container {
    display: grid;
    align-items: center; /* 垂直居中 */
    }
  3. 使用绝对定位和 transform
    可以使用绝对定位和 CSS3 的 transform 属性来实现垂直居中。首先将元素定位到父容器的中心,然后使用 translateY(-50%) 将其向上移动一半高度,从而实现垂直居中。

    1
    2
    3
    4
    5
    .child {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    }
  4. 使用表格布局
    将父容器设置为表格布局,然后使用 vertical-align: middle; 将其内部元素垂直居中。

    1
    2
    3
    4
    5
    6
    7
    8
    .container {
    display: table;
    }

    .child {
    display: table-cell;
    vertical-align: middle; /* 垂直居中 */
    }

这些方法中,Flexbox 是最常用且最推荐的一种方式,因为它简单易用且兼容性良好。根据具体的布局需求和兼容性要求,选择适合的方法来实现垂直居中。

39.flex布局有了解吗

是的,Flexbox 是 CSS3 中引入的一种用于布局的模型,也称为弹性盒子布局(Flexible Box Layout)。Flexbox 提供了一种更加灵活的方式来布局元素,特别适用于构建响应式和动态布局。

Flexbox 的主要特点包括:

  1. 一维布局:Flexbox 是一种一维布局模型,可以控制元素在水平或垂直方向上的排列方式。

  2. 弹性容器和弹性项目:Flexbox 将布局的容器称为弹性容器(flex container),容器内的子元素称为弹性项目(flex item)。通过设置弹性容器和弹性项目的属性,可以实现灵活的布局效果。

  3. 主轴和交叉轴:在 Flexbox 中,主轴(main axis)是弹性容器的主要方向,而交叉轴(cross axis)是与主轴垂直的方向。可以通过设置 flex-direction 属性来指定主轴的方向。

  4. 弹性布局:Flexbox 提供了多种属性来控制弹性项目在主轴和交叉轴上的大小、顺序和对齐方式,包括 justify-contentalign-itemsalign-self 等。

  5. 自适应性:Flexbox 可以根据容器的大小自动调整弹性项目的布局,适应不同屏幕尺寸和设备方向的变化。

  6. 适用范围广:Flexbox 的兼容性良好,适用于现代浏览器以及移动端应用开发。

使用 Flexbox 可以简化网页布局的实现,提高布局的灵活性和响应性,是前端开发中常用的布局方式之一。

40.这个布局要用哪个属性怎么换行

在 Flexbox 布局中,要控制换行可以使用 flex-wrap 属性。该属性用于控制弹性项目是否换行以及换行的方式。

具体来说,flex-wrap 属性有以下几个可能的取值:

  • nowrap(默认值):弹性项目不换行,所有项目都尽可能地放在一行上。
  • wrap:弹性项目在必要的时候换行,第一行在顶部,下一行在其下方。
  • wrap-reverse:弹性项目在必要的时候换行,第一行在底部,下一行在其上方。

示例代码如下:

1
2
3
4
.container {
display: flex;
flex-wrap: wrap; /* 控制弹性项目换行 */
}

在这个示例中,.container 是弹性容器,通过设置 flex-wrap: wrap; 属性,可以使弹性项目在必要的时候换行。

41.css自适应单位

在 CSS 中,自适应单位通常指的是相对长度单位,这些单位相对于视口或者父元素的大小进行自适应调整,可以使得页面在不同设备上有更好的适应性和可读性。常见的自适应单位包括:

  1. 相对长度单位

    • vw(视口宽度单位):相对于视口宽度的百分比,1vw 等于视口宽度的 1%。例如,width: 50vw; 表示元素的宽度为视口宽度的一半。
    • vh(视口高度单位):相对于视口高度的百分比,1vh 等于视口高度的 1%。例如,height: 80vh; 表示元素的高度为视口高度的 80%。
    • %(百分比):相对于父元素的百分比,例如,width: 50%; 表示元素的宽度为父元素宽度的一半。
  2. rem(根元素单位)

    • rem(root em)单位是相对于根元素(html 元素)的字体大小的单位。例如,如果根元素的字体大小为 16px,那么 1rem 就等于 16px。
    • rem 单位的优势在于可以方便地调整整个页面的大小比例,特别适用于响应式设计。
  3. em(相对单位)

    • em 单位是相对于元素自身的字体大小的单位。例如,如果元素的字体大小为 16px,那么 1em 就等于 16px。
    • em 单位可以方便地相对于元素的字体大小进行调整,但是相对于父元素字体大小的计算可能会产生累加效应。

这些自适应单位可以使得网页布局和样式设计更具有灵活性和可读性,有助于实现响应式设计和移动端适配。

42.vh vw有用过吗

是的,vwvh 是视口单位,分别表示相对于视口宽度和视口高度的百分比。它们常用于制作响应式布局或者适配移动设备。

举个例子,假设你希望一个元素的宽度始终是视口宽度的一半,可以这样设置:

1
2
3
.element {
width: 50vw;
}

这样无论用户在什么尺寸的屏幕上查看页面,这个元素的宽度都会自动调整为视口宽度的一半。同样地,如果你希望一个元素的高度始终是视口高度的一半,可以使用 vh 单位。

这些视口单位非常适合在响应式设计中使用,它们使得网页元素能够根据浏览器窗口的大小进行自适应调整,从而提供更好的用户体验。

43.媒体查询

媒体查询(Media Queries)是 CSS3 中的一种技术,它允许你根据设备特性和特定的媒体类型来应用不同的 CSS 样式。媒体查询通常用于实现响应式设计,使得网页能够在不同的设备上以不同的布局和样式呈现,从而适应不同的屏幕尺寸、分辨率、设备方向等。

媒体查询的语法如下:

1
2
3
@media mediatype and (media feature) {
/* CSS 样式规则 */
}

其中,mediatype 可以是以下几种媒体类型之一:

  • all:所有设备。
  • print:打印机和打印预览。
  • screen:电脑屏幕、平板电脑、智能手机等。
  • speech:屏幕阅读器。

media feature 是一个表示设备特性的表达式,常用的包括:

  • width:视口宽度。
  • height:视口高度。
  • orientation:设备方向(横向或纵向)。
  • aspect-ratio:视口的宽高比。
  • resolution:设备屏幕的分辨率。

例如,以下是一个简单的媒体查询示例,当视口宽度小于等于 600px 时应用不同的样式:

1
2
3
4
5
6
@media screen and (max-width: 600px) {
/* 应用于宽度小于等于 600px 的屏幕 */
body {
font-size: 14px;
}
}

媒体查询使得网页能够根据不同设备和浏览器的特性来自适应布局和样式,提供更好的用户体验。

44.css的媒体查询有了解吗

媒体查询(Media Queries)是 CSS3 中的一种功能,它允许你根据设备的特性和特定的媒体类型来应用不同的 CSS 样式。媒体查询通常用于实现响应式设计,使得网页能够在不同的设备上以不同的布局和样式呈现,从而适应不同的屏幕尺寸、分辨率、设备方向等。

媒体查询的语法如下:

1
2
3
@media mediatype and (media feature) {
/* CSS 样式规则 */
}

其中,mediatype 可以是以下几种媒体类型之一:

  • all:所有设备。
  • print:打印机和打印预览。
  • screen:电脑屏幕、平板电脑、智能手机等。
  • speech:屏幕阅读器。

media feature 是一个表示设备特性的表达式,常用的包括:

  • width:视口宽度。
  • height:视口高度。
  • orientation:设备方向(横向或纵向)。
  • aspect-ratio:视口的宽高比。
  • resolution:设备屏幕的分辨率。

例如,以下是一个简单的媒体查询示例,当视口宽度小于等于 600px 时应用不同的样式:

1
2
3
4
5
6
@media screen and (max-width: 600px) {
/* 应用于宽度小于等于 600px 的屏幕 */
body {
font-size: 14px;
}
}

媒体查询使得网页能够根据不同设备和浏览器的特性来自适应布局和样式,提供更好的用户体验。

45.反问
我的表现
后续流程
实习生做什么和技术栈