admin 管理员组文章数量: 887018
- 今天想把近期看的面试题汇总一下,项目空档期,看的面试题比较多,但没有整理,导致回忆的时候,想不起来的还要重新查找。包括身边朋友面试遇到的题目也总结了进去。
- 总结的都是前端基础(初级)面试题。
- 三月份我会持续更新~~~ 浅卷一下下
- 2023.03.09开始——>3.15——>3.20——3.23
一、CSS面试题
1. 三种CSS隐藏元素的方式有哪些?
- 设置display: none;样式。这个样式会让元素在页面上彻底消失。元素本来占有的空间,也会被其他元素占有,所以,他会导致浏览器的重排和重绘。
- 第二种方式设置visibilty:hidden;的样式。它和display: none;的区别在于,元素在页面消失之后,它原本占有的空间依然会保留,所以,他只会导致浏览器的重绘,而不会重排。
- 第三种是设置opcity:0;透明度为0。在视觉上元素也是隐藏的。和设置 visibilty:hidden;一样。
- 还有其他方法,利用定位,让元素不出现在可视区域,等等。
2. CSS 盒模型?
- 盒模型是什么?
- 盒模型的分类?
- 怎么指定盒模型?
1、每个html元素都可以看作一个盒子,这个盒子由里到外,由这个元素的内容content、边框border、内边距padding、外边距margin组成。
2、盒子模型一般分为标准盒模型 和 怪异盒模型,怪异盒模型又叫做IE盒模型。这两种盒模型又有什么区别呢?
3、在标准盒模型下,浏览器的width属性,就是内容content的宽度,也就是说,如果我们给一个元素设置width属性,那么width属性就是内容的宽度,此时这个元素盒子的总宽度就是:width + 内边距 + 边框 + 外边距,高度也是这样。而怪异盒模型,指的是浏览器的width属性不是内容的宽度,是元素的内容 + 内边距 + 边框的宽度之和。
换句话说,如果我们给一个元素设置width属性,那么这个盒子的总宽度就是:width + 外边距 之和。因为width已经包含了内容、内边距、边框。
正常情况下默认是标准盒模型,但是我们可以通过box-sizing属性来指定盒模型,当它的值是border-box时,就是怪异盒模型。当值content-box时,就是标准盒模型,因为标准盒模型的width就是content。
3. 页面布局有哪几种方式?
- 页面布局常用的方法有浮动、定位、flex、grid网格布局、栅格系统布局
- 浮动:优点: 兼容性好。缺点:浮动会脱离标准文档流,因此要清除浮动。我们解决好这个问题即可。
- 绝对定位。优点: 快捷。缺点: 导致子元素也脱离了标准文档流,可实用性差。
- flex 布局 (CSS3中出现的)。优点: 解决上面两个方法的不足,fex布局比较完美。移动端基本用 flex布局。
- 网格布局 (grid)。CSS3中引入的布局,很好用。代码量简化了很多。
- 利用网格布局实现的个左右300px中间自适应的布局:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style>
html * {
padding: 0;
margin: 0;
}
/* 重要:设置容器为网格布局,宽度为100% */
.layout.grid .left-center-right {
display: grid;
width: 100%;
grid-template-rows: 100px;
grid-template-columns: 300px auto 300px; /* 重要:设置网格为三列,并设置每列的宽度。即可。*/
}
.layout.grid .left {
background: red;
}
.layout.grid .center {
background: green;
}
.layout.grid .right {
background: blue;
}
</style>
</head>
<body>
<section class="layout grid">
<article class="left-center-right">
<div class="left">我是 left</div>
<div class="center">
我是 center
<h1>网格布局解决方案</h1>
</div>
<div class="right">我是 right</div>
</article>
</section>
</body>
</html>
4. rem 和 em?
- rem 和 em都是css中的相对长度单位,他们的区别就是,rem是相对于根元素字体大小进行计算的,任意浏览器的默认字体都是16px,所以一般 1rem = 16px。
- 而em是相对于当前元素的字体大小来计算的,但是为什么我们经常说相对于父元素呢,其实也很好理解,因为font-size字体大小这个属性是可以被继承的,所以父元素的字体大小势必会影响到他的子元素的字体大小,也就是说子元素如果没有自己的font-size,那么它的font-size就会继承父元素,那此时 1em 的值就可以认为是相对于父元素字体大小来计算。
- 另外rem是css3新增的一个相对单位,r就是root根的缩写,它的出现也就是为了解决em的缺点,em是相对于父元素或当前元素的字体大小进行换算的,当当前元素或父元素字体大小改变时,就又得重新计算了,那么层级较多的时候,换算也就会越来越复杂,而rem只相对于HTML根元素,有了rem这个单位我们只需要调整根元素HTML的font-size,就能达到所有元素的动态适配,避免了一些复杂的层级关系。
补充:
- rem是相对于根元素进行计算,而em是相对于当前元素或父元素的字体大小。
rem不仅可以设置字体的大小,还支持元素宽、高等属性。 - em是相对于当前元素或父元素进行换算,层级越深,换算越复杂。而rem是相对于根元素计算,避免层级关系。
5. 简单谈一下flex布局?
- 当并列书写多个div标签,它们会纵向向下排位,如果我们想将多个div并列成一排,就得借助position,float,或display属性,这便是传统的盒模型做法。
- 而flex布局则是一种新的布局方案,通过为修改父div的display属性,让父元素成为一个flex容器,从而可以自由的操作容器中子元素(项目)的排列方式。
- 例如我们让多个div横向排列,传统做法是使用浮动,但浮空后因为脱离文档流的缘故,父元素会失去高度,这又涉及了清除浮动等一系列的问题。
- 而flex布局相对简单很多,修改父元素display:flex,你会发现div自动就排列成了一行,而且没有浮动之后的副作用,从回流角度考虑,flex的性能更优于float;随着浏览器不断兼容以及旧版本的淘汰,flex布局注定会成为更为流行的布局方案。
1、flex-direction 属性,决定主轴的方向(即项目的排列方向)
取值:row(默认) | row-reverse | column | column-reverse
2、flex-wrap属性,决定容器内项目是否可换行。
取值:nowrap(默认) | wrap | wrap-reverse
3、justify-content属性,定义了项目在主轴的对齐方式。
取值:flex-start(默认) | flex-end | center | space-between | space-around | space-evenly;
4、align-items属性,定义了项目在交叉轴上的对齐方式。
取值:flex-start | flex-end | center | baseline | stretch(默认)
5、flex-flow: flex-direction 和 f’lex-wrap 的简写形式。
取值:flex-flow: || ;默认值为row nowrap,没什么卵用。
6、align-content: 定义了多根轴线的对齐方式,如果项目只有一根轴线,那么该属性将不起作用。
取值: align-content: flex-start | flex-end | center | space-between | space-around | stretch;
有六种属性可运用在 item 项目上:
- order
- flex-basis: auto;定义了在分配多余空间之前,项目占据的主轴空间,浏览器根据这个属性,计算主轴是否有多余空间
- flex-grow: 0; flex-grow定义项目的放大比例
- flex-shrink: 1; 定义了项目的缩小比例
- flex
- align-self
6. 什么是重排(回流)和重绘?
- 重绘:修改了页面元素的字体颜色、背景颜色、边框颜色,但是并不影响页面布局,因为元素的尺寸大小并没有改变,其他元素也并没有受到位置上的影响,还有设置opcity透明度,visibity:hidden;属性也会导致重绘。总结:当页面元素样式改变而不影响布局时,浏览器重新对元素进行更新的过程叫做重绘。
- 重排:是指修改了元素的尺寸大小,例如改变了元素的宽高、边框的粗细,或者是给某元素设置display:none;属性,这样也会导致回流。总结:当页面元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程叫做重排也叫做回流。所以回流一定会导致重绘。
不管页面发生了重绘还是重排,它们都会影响性能(最可怕的是重排 ,应尽量避免)
7. 哪些情况会发生重排?
- 页面初始渲染
- 添加/删除可见DOM元素
- 改变元素位置
- 改变元素尺寸(宽、高、内外边距、边框等)
- 改变元素内容(文本或图片等)
- 改变窗口尺寸
8. 如何减少重排和重绘(提高性能)?
由于回流和重绘会带来很大的性能开销,所以在开发中我们要尽量避免或减少回流和重绘的次数来提高性能
- 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
- 对具有复杂动画的元素使用绝对定位,使其脱离文档流,否则会引起父元素及后续元素频繁回流。
- 要避免频繁的去操作DOM,可以通过创建documentFragment,完成所有所有DOM操作后,最后再把它添加到文档中。
- 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
9. 页面渲染的过程?(浏览器是怎么渲染页面的?)
- 解析HTML代码结构生成 DOM 树。就是将代码解析为一颗DOM Tree,按照从上到下,从左到右的顺序,将树上的节点压入文档流然后布局。
- 解析CSS文件,生成CSSOM(css Object Model)。顺序为:浏览器默认样式->自定义样式->页面内的样式。
- 将DOM和CSSOM结合起来就形成了Render树。这个渲染树和DOM树的不同之处在于,它是受样式影响的。它不包括那些不可见的节点。
- 当渲染树生成之后,浏览器就会根据渲染树在屏幕上渲染和展示。
其中解析HTML的时候会将HTML文档转换为DOM树,构建render树的时候会将DOM树转换为更加结构化的渲染树,布局和绘制render树的时候会将渲染树转换为可见的像素画面。
10. 如何水平垂直居中?
1. 通过定位,给父盒子相对定位,子盒子绝对定位,top、left为50%,再margin-left : -(子盒子宽的一半)px; margin-top: -(子盒子高的一半)px;
<style>
div {
position: relative;
height: 400px;
width: 400px;
background-color: pink;
}
span {
position: absolute;
top: 50%;
left: 50%;
margin-left: -50px;
margin-top: -50px;
display: block;
width: 100px;
height: 100px;
background-color: purple;
}
</style>
2. 不依赖通过计算子盒子的宽高进行定位,可以用transform: translate 移动自身的一半就行了。
<style>
div {
position: relative;
height: 400px;
width: 400px;
background-color: pink;
}
span {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: block;
width: 100px;
height: 100px;
background-color: purple;
}
</style>
3. 通过flex布局,设置垂直水平都居中。
<style>
div {
display: flex;
justify-content: center;
align-items: center;
height: 400px;
width: 400px;
background-color: pink;
}
span {
display: block;
width: 100px;
height: 100px;
background-color: purple;
}
</style>
11. 移动端适配
- 之前做前端适配,要么通过使用css的media媒体查询,要么通过获取可视宽度按照想要的比例设置文档根字体大小。无意间在网上看到一种新的适配插件,postcss-px-to-viewport,就是用viewport(vw)单位,viewport单位越来越受到众多浏览器的支持,postcss-px-to-viewport,将px单位按照相应的配置自动转换成需要的viewport单位。
- 使用插件:postcss-px-to-viewport。可使用npm、yarn安装:npm install -D postcss-px-to-viewport 或 yarn add -D postcss-px-to-viewport 。
- 在项目的最外层(即配置文件同级目录下)新建.postcssrc.js文件,在.postcssrc.js文件中加入配置语句,.postcssrc.js更新需要重启才会生效,以下为示例:
module.exports = {
plugins: {
autoprefixer: {}, // 自动添加相应浏览器前缀,样式兼容,如-webkit-,-moz-等
"postcss-px-to-viewport": {
unitToConvert: "px", // (String)需要转换的单位,默认为"px"
viewportWidth: 750, // (Number)设计稿的视口宽度:可为设计稿的宽度,也可按需自行配置(PC)
unitPrecision: 6, // (Number)单位转换后保留的精度,即小数点位数
propList: ["*"], // (Array)指定转换的css属性的单位,*代表全部css属性的单位都进行转换
viewportUnit: "vw", // (String)指定需要转换成的视窗单位,默认vw
fontViewportUnit: "vw", // (String)指定字体需要转换成的视窗单位,默认vw
selectorBlackList: ["demo"], // (Array)需要忽略的CSS选择器,不转为视窗单位,使用原有单位,
minPixelValue: 1, // (Number)设置最小的转换数值,默认为1,只有大于1的值会被转换
mediaQuery: true, // (Boolean)媒体查询里的单位是否需要转换单位,默认false
replace: true, // (Boolean)是否直接更换属性值,而不添加备用属性
exclude: [/node_modules/], // (Array or Regexp)忽略某些文件夹下的文件或特定文件,用正则做目录名匹配
include: /Ignore.vue/, //(Array or Regexp)只有匹配到的文件才会被转换,那将只有匹配到的文件才会被转换
landscape: false, // (Boolean)是否添加根据 landscapeWidth 生成的媒体查询条件
landscapeUnit: 'vw', //(String)横屏时使用的单位
landscapeWidth: 700 //(Number)横屏时使用的视口宽度
}
}
};
- 也可在配置文件中进行配置 vue.config.js。
const pxtoviewport = require("postcss-px-to-viewport");
module.exports = {
css: {
// 忽略 CSS order 顺序警告
extract: { ignoreOrder: true },
// 适配插件
loaderOptions: {
postcss: {
plugins: [
pxtoviewport({
// 配置视窗口尺寸
viewportWidth: 1700,
unitPrecision: 5,
viewportUnit: "vw",
fontViewportUnit: "vw",
selectorBlackList: [],
minPixelValue: 1,
mediaQuery: true,
}),
],
},
},
},
}
- 使用 lib-flexible 动态设置 REM 基准值(html 标签的字体大小)
- 安装: yarn add amfe-flexible 或者 npm i amfe-flexible
- 然后在 main.js 中加载执行该模块:import ‘amfe-flexible’
- 最后测试:在浏览器中切换不同的手机设备尺寸,观察 html 标签 font-size 的变化。
- 使用 postcss-pxtorem 将 px 转为 rem
- 安装:yarn add -D postcss-pxtorem,npm install postcss-pxtorem -D。( -D 是 --save-dev 的简写。)
- 然后在项目根目录中创建 .postcssrc.js 文件。
- 配置完毕,重新启动服务。
module.exports = {
plugins: {
'postcss-pxtorem': {
rootValue: 37.5,
propList: ['*']
}
}
}
12. 什么是BFC?其规则是什么?怎么触发BFC?BFC能够解决什么问题?
【 什么是BFC?】BFC的全称是block-formatting-context, 对应其中文翻译就是块级格式上下文,它是一个独立的渲染区域,我们可以把BFC理解为,一个封闭的容器,内部的元素无论怎么变化都不会影响到外部,容器内的样式布局自然也不会受到外界的影响。
【BFC内部规则】1BFC它就是一个块级元素,块级元素会在垂直方向上一个接一个的往下排列,2BFC就是页面中的一个隔离的独立容器,容器里的标签不会影响到外部标签,3BFC区域不会与浮动的容器发生重叠,4属于同一个BFC的两个相邻元素的外边距会发生重叠,垂直方向的距离由两个元素中margin的较大的值决定,5计算BFC的高度时,浮动元素也会参与计算。
【如何触发BFC? 】通过添加CSS属性,就可以触发,overflow:hidden;除了visible以外的值,position:absolute/fixed;display:inline-block/flex;
【BFC到底解决什么问题】?
- 它可以阻止元素被浮动元素覆盖,例如,一个两栏布局,左边div宽度固定,同时设置左浮动,右边的div自适应,此时由于浮动元素脱离文档流了,不占据空间,那么就会导致右侧的div到了最左边,同时左侧浮动的div还会覆盖在上面,这时候我们就可以通过把右侧的div元素设置为一个BFC,比如可以给它添加display:flex;属性来触发,就可以解决右侧被左侧覆盖的问题。
- 能够解决父元素没有高度,子元素设置成浮动元素时,产生父元素高度塌陷问题,比如一个容器内的两个div都是浮动元素,此时我们给父元素添加一个红色的背景色,会发现没有任何效果,因为父元素高度塌陷,高度为0,这个时候我们就可以添加一个触发BFC功能的属性,因为BFC有个规则是计算BFC高度时,浮动元素也会参与计算,所以触发BFC后,父元素的高度就会被撑开,也就是会产生清除浮动的效果。
- 第三可以解决margin边距重叠的问题,比如一个容器里有两个div,一个div的下边距的margin设置的是10px,一个div的上边距设置的是20px,那这两个盒子之间的距离是20px,而不是30px,这就是margin塌陷问题,这个时候margin应为两个div之间较大的那个margin值,而不是两者相加,如果就想让他们之间的间距是30px,就需要触发一个div的BFC,它的内部就会遵循BFC规则,解决办法是为元素包裹一个盒子,形成一个完全独立的空间,做到里面的元素,不被外面的元素影响。
13. 伪类和伪元素的区别?
- 在 CSS3 中,规定了伪类用单冒号
(:)
表示,伪元素用双冒号表示(::)
,对于css3之前已经存在的伪元素,也是支持单冒号的,但我们在开发过程中还是要尽量规范写法。
【区别】
伪类:当我们希望样式在某些特定状态下才被呈现到指定的元素时,换句话说就是,当某个元素状态改变时,我们期待给这个元素添加一些特殊效果,那么我们就可以往元素的选择器后面加上对应的伪类。比如: hover就能够指定当我们悬浮在某元素上时,期望该元素要显示的样式。
伪元素:则是创建了一些不在文档树中的元素,并为其添加样式,需要注意的是伪元素样式里必须要给它一个content属性。比如可以通过::before伪元素在个元素前增加一些文本,并为这些文本添加样式。这些文本实际上不在文档树中的,所以叫伪元素。
总结来看,伪类的操作对象是文档树中已有的元素,而伪元素则是创建文档树以外的元素并为其添加样式。所以二者最核心区别就在于,是否创造了“新的元素"。
<template>
<div>
<div class="div1">第一个字符是红色。</div>
<div class="div2">选择部分背景是黑色哟。</div>
<div class="div3">该内容前面插入内容123。</div>链接被访问后是红色,悬浮是绿色。<div>
<p>第一个p元素会是黄色</p>
<p>第一个p元素会是黄色</p>
</div>
</div>
</template>
<style scoped>
/*单冒号(是伪类,双冒号《::》是伪元素*/
.div1::first-letter{
color: red;
}
.div2::selection{
background-color: black;
}
.div3::before{
content:'123';
}
a:visited {
color: red;
}
a:hover{
color: limegreen;
}
p:first-child {
color:yellow;
}
<style>
14. scoped的作用?
- 当style标签拥有scoped属性时,它的CSS样式就只作用于当前的组件。也就是说,scoped可以使得组件之间的样式互相隔离,互不影响。如果一个项目中的所有style标签全部加上了scoped,就相当于实现了样式的模块化。
- 【原理】添加scoped编译后,会给组件内所有标签元素添加一个唯一标识,这个唯一标识就是自定义属性
data-v-xxxxxx(8个随机数)这样的字眼,同时,对应的样式选择器也会添加上这个唯一的属性选择器,由于每个组件的标识都是唯一的,意味着不会有重复的属性选择器出现,所以能够实现组件间的样式隔离。 - 对于data-v-xxxxxx这个唯一标识,可以运行一个vue项目,f12后元素检索就能看到每个元素都带了一个这样的标识。
15. 纯css画一个三角形?
- css 画三角形的原理是利用盒子边框完成的,实现步骤可以分为以下四步: 1设置一个盒子 2设置四周不同颜色的边框 3将盒子宽高设置为 0,仅保留边框 4得到四个三角形,选择其中一个三角形后,将其他三角形的边框颜色设置颜色为透明。
但这种方式,虽然视觉上是实现了三角形,但实际上,隐藏的部分任然占据部分高度,需要将上方的宽度去掉。
.triangle {
width: 0px;
height: 0px;
border-top: 100px solid transparent;
border-bottom: 100px solid red;
border-left: 100px solid transparent;
border-right: 100px solid transparent;
}
<div class="triangle"></div>
- 利用伪元素画一个三角形
#triangle {
margin: 100px;
/* width: 100px;
height: 100px; */
background-color: pink;
position: relative;
}
#triangle::before {
position: absolute;
content: "";
width: 0;
height: 0;
top: 0px;
left: 100px;
border-top: solid 50px transparent;
border-left: solid 50px pink;
border-bottom: solid 50px transparent;
}
<div id='triangle'></div>
二、js面试题
1. 节流和防抖?
- 防抖,就是在连续的操作中,无论进行了多长时间,只有某一次的操作后,在指定的时间内没有再操作,这一次才被判定为有效。(事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。)
- 防抖的应用场景:搜索框输入关键字的过程中,实时请求服务器匹配搜索结果,如果不进行处理,输入框内容一直发生变化,导致一直发送请求。如果进行防抖处理,结果就是,当我们输入内容完成后,一定时间内(比如500ms),没有再输入内容,这时再触发请求。
// 1. 利用计时器,实现防抖。
// 2. 通过 setTimeout 的方式,在一定的时间间隔内,将多次触发变成一次触发。
function debounce(fn, delay) {
let timer;
return function () {
let args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(()=> {
fn.apply(this, args);
}, delay);
};
}
- 节流的意思是,在频繁的操作中,在规定时间内,只触发一次。比如我们设定500ms,在这个时间内,无论点击按钮多少次,它都只会触发一次。
- 节流的应用场景:1.抢购的时候,无数人快速的点击按钮,如果每次点击都发送请求,就会给服务器造成巨大的压力,但是我们进行节流后,就会大大减少请求的次数。2.防止表单提交按钮被多次触发,我们应该选择使用节流而不是防抖。3. 滚动加载,加载更多或滚到底部监听。
function throttle(fn, delay) {
let timer;
return function () {
let _this = this;
let args = arguments;
if (timer) {
return;
}
timer = setTimeout(function () {
fn.apply(_this, args);
// 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器
timer = null;
}, delay)
}
}
2. 内存泄漏如何处理?
- 内存泄漏就是指由于疏忽或者程序的某些错误造成未能释放已经不再使用的内存的情况。简单来讲就是假设某个变量占用100M的内存,而你又用不到这个变量,但是这个变量没有被手动的回收或自动回收,仍然占100M的内存空间,这就是一种内存的浪费,就是内存泄漏。(内存泄漏是指在程序运行时,申请的内存空间没有被及时释放,导致程序占用的内存不断增加,最终可能导致系统崩溃或变得非常缓慢。)
- 在前端开发中,内存泄漏可能会发生在 js代码中。例如,在使用闭包时,如果闭包中引用了外部函数的变量,而这个外部函数已经执行完毕,但是闭包依然存在,那么这个外部函数占用的内存就无法被释放,可能导致内存泄漏。
- 为了避免内存泄漏,开发人员应该及时释放不再使用的变量和资源,避免出现循环引用等情况,并使用一些工具和技术来检测和解决内存泄漏问题。
(1) 绑在 EventBus 的事件没有解绑
(2)全局变量造成的内存泄露,声明的全局变量在切换页面的时候没有清空。
(3)遗忘的定时器
(4)控制台的打印
(5)闭包使用不当引起内存泄漏
(6)v-if 指令产生的内存泄露
3. 谈谈你对浅拷贝和深拷贝的理解?
- 浅拷贝就是通过赋值的方式进行拷贝,只拷贝对象的引用,是对指针的拷贝,拷贝后两个指针指向同一个内存,同一份数据,意味着当原对象发生变化的时候,拷贝对象也跟着变化。
什么时候使用深拷贝?:后台返回数据,我们需要对数据进行操作,可能其他地方也需要使用这份数据,直接修改会导致很多其他隐性问题,使用深拷贝,就能让我们更安全,更安心的去操作这些数据了,因为反正我们复制了一份下来。
- 深拷贝:不但对指针进行拷贝,而且还对指针指向的内容进行拷贝,也就是另外申请了一块空间内存,内容和原对象一致,但是是两份独立的数据,更改原对象,拷贝对象不会发生变化。
- 大白话:假设B复制了A,当修改A时,如果B也跟着变了,说明只拷贝了指针,A,B实际共用一份数据,这是浅拷贝;如果A变,B没变,那就是深拷贝,复制对象不受原对象影响。因为不仅拷贝了指针,还拷贝了内容,他们自己有自己的内存,互相独立。
深拷贝的实现方式
(1) js内置对象的JSON对象的序列化和反对象化方法:JSON.parse + JSON.stringify
// 但是这个方法是有局限的,无法实现对对象中方法的深拷贝,取不到值为undefined的key等等之类的。
function cloneDeepJson(obj){
return JSON.parse(JSON.stringify(obj))
}
(2) 使用递归实现深拷贝。a. 传入的原对象,遍历其属性,每个属性需要判断它的值是否是object类,如果不是,说明是基本数据类型,可以直接赋值;b. 如果是object类,那么需要再具体判断这个数据是对象还是数组,是数组创建一个空数组[],是对象则创建一个空对象{},继续递归。
//递归实现深拷贝
function deepClone(origin, target){
var target = target || {}; //防止不传target
for(let key in origin){ //遍历origin中的属性
if(origin.hasOwnProperty(key)){ //判断自身是否有该属性而非原型链上的
if( origin[key] && typeof(origin[key]) == "object"){ //如果当前value是一个object类型,需要往下递归
target[key] = Array.isArray(origin[key]) ? [] : {}; //判断这个object类型,具体是数组还是对象
deepClone(origin[key], target[key]); //递归
}else{
target[key] = origin[key]; //如果当前value不是一个object类型,直接赋值即可
}
}
}
return target; //返回最终的拷贝对象
}
(3) loadash是一个很热门的函数库,我们引入这个库后,就可以直接使用这个方法了,但是如果项目本身没有引入这个库,就不要为了使用深拷贝专门引入整个库,这样有点得不偿失。
(2) Object.assign(target,…sources) ES6新增的方法可以实现深拷贝,target: 拷贝给谁。 sources: 拷贝的对象。
// 注意:只有当对象中没有嵌套对象时,才可以实现深拷贝
const foo = {
name: '张三',
age: 24
}
const newFoo = Object.assign({}, foo)
foo.age = 25
console.log(foo, newFoo) // {name: '张三', age: 25} {name: '张三', age: 24}
// 对象中有内嵌的对象时
const foo = {
name: '张三',
info: {
age: 24
}
}
const newFoo = Object.assign({}, foo)
foo.info.age = 25
console.log(foo, newFoo) // { name: '张三', info: { age: 25 } } { name: '张三', info: { age: 25 } }
(3) for ··· in ,使用for ··· in 遍历赋值太麻烦,不推荐。只要碰到某一个复杂数据类型(数组、对象),就再次进入这个复杂数据类型进行二次遍历,如果还有就再进入继续遍历。
(4) 通过 json 反序列化实现深拷贝。不管多复杂的数据类型,转换为json字符串以后就是基本数据类型,字符串的赋值就是基本数据类型的赋值,赋值以后再转换回来。
(6) ...
展开运算符。拷贝一层是深拷贝,但是修改对象里数组的某一项就会受影响,说明是浅拷贝。
let obj11 = {
name: "张三",
age: 18,
hobbies: ['sleep', 'eat', 'play']
}
let obj12 = { ...obj11 }
obj12.name = "胡歌"
obj12.hobbies[0] = 'sleep + doDream'
console.log('--------------------', obj11, obj12);
(7) arr.concat() 方法。只深拷贝最外层,修改数组里的对象就会受影响,是浅拷贝。
// 该方法用于数组合并,合并的数组.concat(被合并的数组元素…)
// 参数可有多个,用逗号分隔,返回合并后的数组。
// 原理:用原数组去合并一个空数组,返回合并后的新数组。
let arr11 = [1, 3, {
username: 'coco'
}];
let arr22 = arr11.concat();
arr22[2].username = 'xxxxx';
arr22[0] = 555;
// console.log(arr, 'arr');1
console.log('arr11---------------', arr11);
console.log('arr22--------------', arr22);
(8) arr.slice() 方法。只深拷贝最外层。
const arr1 = [1, 3, { username: 'aaaaaa' }]
const arr2 = arr1.slice() //返回截取后的数组,这里没有截取掉任何项,相当于返回原数组。
// 修改堆内存中的值
arr2[0] = 5
arr2[2].username = 'bbbbbb'
console.log('arr1---', arr1)
console.log('arr2---', arr2)
https://zhuanlan.zhihu/p/556923120 深入了解深浅拷贝
4. 什么是冒泡,如何解决冒泡?什么是事件代理(事件委托)?
- 指父元素和子元素有相同的事件,当触发子元素事件时,会向上冒泡,同时也会触发父元素事件。
阻止冒泡:通过 event.stopPropagation() 方法阻止事件冒泡到父元素,阻止任何父事件处理程序被执行。
- 不需要给每一子节点都绑定事件,而是将子节点的事件绑定到他们共同的结构父级接节点上,这样在点击每一个子节点的时候,会利用事件传播冒泡执行父节点上绑定的事件。
5. axios是什么? axios是怎么封装的?
- axios是一个封装好的独立的ajax请求库, 基于Promise。支持在浏览器和Node中使用。
axios封装的步骤:
- 在根目录创建 utils 文件夹
- 创建 request.js文件
- 在request.js里引入 axios
- 配置 基本路径和超时时间
- 配置请求拦截和响应拦截
- 在请求拦截里可以放 loading 和 token
- 在响应拦截中 可以 清除 loading 还有处理错误编码字典
- 最后把我们封装的 axios 实例 导出
6. es6的新特性用到哪些?
- 声明变量的的关键字:let;声明常量的const。
- 解构赋值
- 模板字符串
- 箭头函数
- 扩展运算符
- promise 用来封装异步操作并且可以获取成功或失败的结果。
7. 什么是闭包?优缺点是什么?
- 闭包指的是有权访问另外一个函数作用域中变量的函数。
- 优点: 1. 一个是可以读取外层函数内部的变量。2. 让这些变量始终保存在内存中,即闭包可以使它的诞生环境一直存在(延伸了变量的作用范围。)
- 缺点: 容易造成内存泄漏。
- 满足以下条件才是闭包:1有函数嵌套2内部函数引用外部作用域的变量3返回值是一个函数4创建一个对象函数,让其长期驻留
- 为什么需要使用闭包?因为全局变量容易污染环境,而局部变量又无法长期驻留内存,于是我们需要一种机制,既能长期保存变量又不会污染全局,这就是闭包。
- 什么时候使用闭包?:当我们需要重复使用一个对象,又想保护这个对象不被其他代码污染的时候,就可以使用闭包。
- 如何销毁闭包的内存空间?将外部接收函数的变量重新赋值为null即可。
// 写一个函数,每次调用,数字都要减1 --------- 全局变量,占用内存,任意函数(地方)都可以随便调用,会污染环境。
var a = 100;
function fn() {
a--;
console.log(a);
}
fn() // 99
fn() // 98
fn() // 97
// -----------每次调用,结果都是99。每次函数一调用完就会销毁。每次调用,b都是100开始,存不住。
function fn2() {
var b = 100;
b--;
console.log(b);
}
fn2() // 99
fn2() // 99
fn2() // 99
// ------------闭包的写法
function fn3() {
let c = 100;
function fn4() {
c--;
console.log(c);
}
return fn4;
}
var fn5 = fn3();
fn5() // 99
fn5() // 98
fn5() // 97
fn5 = null;
8. 浏览器的同源策略(跨域问题)有了解过吗?
- 如果一个请求url的 协议、域名、端口三者之间任意一个与当前页面url不同就会产生跨域的现象。
- 同源策略,是一种网页的安全协议。同源:同协议,同域名,同端口。
- http;// www. aaa:8080/index/vue.js ,协议子域名 主域名 端口号资源。
- 同源策略是浏览器的核心,如果没有这个策略就会遭受网络攻击。
- 主要指的就是协议+域名+端口号三者一致,若其中一个不一样则不是同源,会产生跨域。
- 三个允许跨域加载资源的标签: img、 link、 script 跨域是可以发送请求,后端也会正常返回结果,只不过这个结果被浏览器拦截了!
- vue-cli代理跨域:在 vue.config.js文件的devServer对象的 proxy中配置。
module.exports={
pages:{
index:{
//入口
entry:'src/main.js',
},
},
lintOnSave:false,//关闭语法检查
//开启代理服务器 方法一
// devServer:{
//paroxy:'http://localhost:5000',//5000服务器端口号,
// },
//方法二:
devServer:{
proxy:{
// 请求前缀/api,只有加了/api前缀的请求才会走代理(前端自定义)
'/api':{
target:'http://localhost:5000',
pathReweite:{'^/api':''},//重写
//ws:true,//用于支持websocket
changeOrigin:true,//用于控制请求头中的host值,默认true,react中默认false
}
}
}
}
在使用webpack或者vite构建工具时候,配置反向代理,把本地所有的axios请求的地址,(域名:端口)改成:/api/,本地发起请求的地址 /api/ 都会被自动替换成实际后端请求地址;这样就实现了当前请求的地址,就会被后端接收识别为和前端都一样的域名;保证了前后端域名一致,解决了跨域;
2. nginx里面也可以使用同样的方式,通过反向代理转发,来解决跨域问题。
3. JSONP解决跨域。需要前后端一起配合,利用同源策略对script标签不受限制,不过只支持get请求。
4. CORS跨域资源共享。cors跨域,只需要后端配置,服务端设置header(“Access-Control-Allow-Origin:*”);允许任何来源,header(“Access-Control-Allow-Origin:http://me”);只允许来自域名http://me的请求。
9. 前端登陆流程
- 在登录页点击登录的时候,前端会带着用户名和密码去调用后端的登录接口。
- 后端收到请求,验证用户名和密码,验证失败,会返回错误信息,前端提示相应错误信息,如果验证成功,就会给前端返回一个token。
- 前端拿到token,将token储存到Vuex和localStorage中,并跳转页面,即登录成功。
- 前端每次跳转至需要具备登录状态的页面时,都需要判断当前token是否存在,不存在就跳转到登录页,存在则正常跳转(通常封装在路由守卫中)。
- 另外,在向后端发送其他请求时,需要在请求头中带上token(项目中通常封装在请求拦截器中),后端判断请求头中有无token,有则验证该token,验证成功就正常返回数据,验证失败(如已过期)则返回相应错误码。前端拿到错误信息,清除token并回退至登录页。
11. New操作符做了什么?
- 第一步创建一个空对象。
- 第二步将 this 指向空对象。
- 第三步动态给刚创建的对象添加成员属性。
- 第四步隐式返回 this。
12. 箭头函数与普通函数的区别?箭头函数能当构造函数吗?
1. 箭头函数比普通函数在写法上更加简洁。
(1)如果没有参数,就直接写一个空括号即可
(2) 如果只有一个参数,可以省去参数的括号
(3)如果有多个参数,用逗号分割
(4)如果函数体的返回值只有一句,可以省略大括号
2. 箭头函数没有自己的this
箭头函数不会创建自己的this, 所以它没有自己的this,它只会在自己作用域的上一层继承this。所以箭头函数中this的指向在它在定义时已经确定了,之后不会改变。
3. 箭头函数继承来的this指向永远不会改变,call()、apply()、bind()等方法不能改变箭头函数中this的指向 。
var id = 'GLOBAL';
var obj = {
id: 'OBJ',
a: function(){
console.log(this.id);
},
b: () => {
console.log(this.id);
}
};
obj.a(); // 'OBJ'
obj.b(); // 'GLOBAL'
new obj.a() // undefined
new obj.b() // Uncaught TypeError: obj.b is not a constructor
对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象。需要注意,定义对象的大括号{ }是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中。
4. 箭头函数不能作为构造函数使用
由于箭头函数时没有自己的this,且this指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。
5. 箭头函数没有自己的arguments
箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。
6. 箭头函数没有prototype
7. 箭头函数的this指向哪⾥?
箭头函数不同于传统JavaScript中的函数,箭头函数并没有属于⾃⼰的this,它所谓的this是捕获其所在上下⽂的 this 值,作为⾃⼰的 this 值,并且由于没有属于⾃⼰的this,所以是不会被new调⽤的,这个所谓的this也不会被改变。
13. 什么是伪数组(又叫对象数组)?它和数组有什么区别?
- 伪数组数据类型是object对象,而真实的数组数据类型 array。
- 伪数组有length属性,索引,可以通过.length获取长度,并且可以通过索引获取某个元素,但没有forEach等方法,而数组拥有数组全部方法。
- 伪数组长度不可以改变,数组长度可以改变。
- 伪数组因为是对象数组所以用 for… in遍历,数组更建议用for … of。
- 伪数组转数组用 array.from或直接展开运算符展开在一个新数组里。
- 在JavaScript中常见的伪数组就是arguments,函数的参数 arguments;另外还有,原生JS获取dom元素,document.querySelector(“div”) 获取到的列表也是一个伪数组。
14. 怎么把类数组转换为数组?
- 通过Array.from方法来实现转换
Array.from(arrayLike) - 通过call调用数组的slice方法来实现转换
Array.prototype.slice.call(arrayLike) - 通过call调用数组的splice方法来实现转换
Array.prototype.splice.call(arrayLike,0) - 通过apply调用数组的concat方法来实现转换
Array.prototype.concat.apply([],arrayLike)
15. 说一下常见的检测数据类型的几种方式?
- typeof 其中数组、对象、null都会被判断为Object,其他判断都正确。
- instanceof 只能判断引用数据类型,不能判断基本数据类型。
- constructor 它有两个作用 ,一是判断数据的类型,二是对象实例通过constructor对象访问它的构造函数。需要注意的事情是如果创建一个对象来改变它的原型,constructor就不能来判断数据类型了。
- Object.prototype.toString.call() 使用 object 对象的原型方法 tostring 来判断数据类型。
instanceof和typeof的区别:
instanceof:返回值为布尔值。instanceof 用于判断一个变量是否属于某个对象的实例。
typeof:返回值是一个字符串, 用来说明变量的数据类型。typeof 一般只能返回如下几个结果:number,boolean, string, function, object, undefined
16. 数组去重有几种方法?
1.最简单的set去重
var aa=[23,45,23,23,34,2,34,66,78];
var aa=new set(aa);
console.log(aa);
2.用indexOf()来去重
var sum=[ ]; //给它一个空的数组
for(var i=o;i<aa.length;i++){
//如果没有找到字符串,则返回-1,检索是否存在。
if(sum.indexOf(aa[i]>=0){
continue;//结束本次循环
}else{
sum.push(aa[i]);
}
}
console.log(sum);
3.用sort排序后去重
function fn(arr){
let newArr = []
arr.sort((a,b)=>{
return a-b
})
arr.forEach((val,index)=>{
if(val != arr[index+1]){
newArr.push(val)
}
})
return newArr;
}
4.数组去重,根据里面相同的ID去重,键值
var str=[
{name:"张三",id:1},
{name:"李四",id:2},
{name:"王五",id:2},
{name:"小明",id:3},
{name:"小兰",id:1},
];
//声明一个数组
var result=[];
//声明一个对象
var obj={};
for(var i=0;i<str.length;i++){
if(!obj[str[i].id]){
result.push(str[i]);
obj[str[i].id]=true;
}
}
console.log(result);
5.普通去重的方式
function removeRepeat(arr) {
for(var i=0;i<arr.length;i++){
for(var j=i+1;j<arr.length;j++){
if(arr[i]==arr[j]){
arr.splice(j,1); //索引,删除长度
j--;
}
}
}
console.log(arr);
return arr;
}
removeRepeat([23, 45, 23, 23, 34, 2, 34, 66, 78])
17. 如何判断一个对象是空对象?
// 1.使用JSON自带的.stringify 方法来判断
if(Json.stringify(obj)=='{}'){
console.log('是空对象')
}
// 2.使用ES6新增的方法Object.keys()来判断
if(Object.keys(Obj).length<0){
console.log('是空对象')
}
18. for…in 和 for…of的区别?
- for…in 遍历对象获取的是对象的键名,for…in 遍历数组获取的是数组的索引值。for…of 遍历对象报错:obj is not iterable,for…of遍历数组得到的是数组的每一项值。
- for…in会遍历对象的整个原型链,性能非常差不推荐使用。而for…of 只遍历当前对象不会遍历原型链。
- 对于数组的遍历,for…in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of 只返回数组的下标对应的属性值。
总结:for…in循环主要是为了遍历对象而生,不适用遍历数组。 for…of 循环可以用来遍历数组、类数组对象、字符串、Set、Map以及Generator对象。
19. JSON.stringify有什么缺点?
1.如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将是字符串的形式,而不是对象的形式。
2. 如果obj里有RegExp(正则表达式的缩写)、Error对象,则序列化的结果将只得到空对象。
3. 如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失。
4. 如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null。
5. JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor。
6. 如果对象中存在循环引用的情况也无法正确实现深拷贝。
20. Set的用法?
一、Set数据结构的特点?
- ES6提供了新的数据结构Set,它类似于数组,但是成员的值都是唯一的,没有重复的值。
- Set不同于Map,它没有键值对的概念,它只有一个维度,就是value。
二、Set数据结构应用在哪些地方?
- 应用:搜索历史关键字的存储
- 网站的搜索功能,用户搜索完成后,网站要记录用户搜索的关键字,方便用户下次直接点击,搜索历史关键字就可以了。
- 用户搜索历史关键字,不能有重复的值,当用户多次输入相同的关键字,用set存储值,内部会自动判断值是否重复,如果重复,就不会再存储。
三、Set的用法
- Set本身是一个构造函数,用来生成Set数据结构。
- 属性size,Set里面有多少个元素,类似于数组的length。
const set = new Set();
const s1 = new Set(); // 空的Set数据结构
console.log(s1.size) // 0
const s2 = new Set(["a", "b"]);
console.log(s2.size) // 2
- Set函数可以接受一个数组作为参数,用来初始化。
1.
const set = new Set([1,2,3,4,5,4,5]);
console.log(set) // {1, 2, 3, 4, 5}
2.
const s3 = new Set(["a","a","b","b"]);
console.log(s3.size) // 2
const ary = [...s3];
console.log(ary) // ["a","b"]
四、Set的属性和方法(实例方法)
1. add(value): 添加某个值,返回Set结构本身。
const s4 = new Set();
// 向set结构中添加值 使用add方法
s4.add('a').add('b'); // 链式调用
console.log(s4.size) // 2
2. delete(value): 删除某个值,返回一个布尔值,表示删除是否成功。
const s4 = new Set(['1','2','c']);
const r1 = s4.delete('c');
console.log(s4.size) // 2
console.log(r1); // true
3. has(value): 返回一个布尔值,表示该值是否为Set的成员。
const s4 = new Set(['1','2','c']);
const r2 = s4.has('d');
console.log(r2) // false
4. clear(): 清除所有成员,没有返回值。
const s4 = new Set(['1','2','c']);
s4.clear();
console.log(s4.size); // 0
5. 遍历 (方法)
- Set结构的实例与数组一样,也拥有forEach方法,用于对每个成员执行某种操作,没有返回值。
// 遍历set数据结构,从中取值
const s5 = new Set(['a', 'b', 'c']);
s5.forEach(value => {
console.log(value)
})
// a
b
c
21. Map的用法
一、Map数据结构是用来干嘛的?
- Map对象用来存储键值对。
- Map本身是一个构造函数,用来生成Map数据结构。
// 创建一个Map
const map = new Map();
二、Map的属性和方法(实例方法)
1. size属性
- 返回Map对象中所包含的键值对的个数。
const map = new Map()
const a = map.set(1,'one')
console.log(a.size) // 1
2. set方法
- 向Map中添加新元素。
- 第一次set是添加数据,第二次添加相同的key时,会将第一次添加的值给覆盖掉。
set(key,value)
const map = new Map()
const a = map.set(1,'one')
console.log(a) // {1 => 'one'}
3. get方法
- 通过键,查找特定的值。
get(key)
4. delete方法
- 通过键,从Map中移除对应的数据。
delete(key)
5. has方法
- 判断Map对象中是否存在key,若有则返回true,否则返回false。
has(key)
6. clear方法
- 将这个Map中的所有元素删除。
clear(key)
三、如何遍历Map
1. keys(): 返回键名的遍历器
const map = new Map()
map.set(1,'one')
map.set(2,'two')
map.set(3,'three')
for(const item of map.keys()) {
console.log(item); // 1 2 3
}
2. values(): 返回键值的遍历器
const map = new Map()
map.set(1,'one')
map.set(2,'two')
map.set(3,'three')
for(const item of map.values()) {
console.log(item); // 'one' 'two' 'three'
}
3. entries(): 返回键值对的遍历器
- entry条目的意思,entries复数,意思指键值对。
- entries方法中的键值对是以数组的形式存在的。
const map = new Map()
map.set(1,'one')
map.set(2,'two')
map.set(3,'three')
for(const item of map.entries()) {
console.log(item); // 返回三个数组[1,'one'] , [2,'two'], [3,'three']
}
4. forEach(): 使用回调函数遍历每个成员
第一个参数:item,是Map的value值
第二个参数:index 是Map的key值
第三个参数: Map本身
22. js的数据类型
- is中数据类型分为基本数据类型和引用数据类型。
- 基本数据类型有Number,String,Boolean,Null,Undefined,es6新增的Symbol以及es10新增的Biglnt(任意精度整数)七类。
- 引用数据类型即object类,比如对象数组,以及函数。
- 存储位置:基本类型存储在栈内存中而引用数据类型在栈内存中存的是对象指针,这个指针指向堆内存中的值,也就是说实际值是存在堆内存中,栈中存的是对象在堆内存中的引用地址,通过这个引用地址可以找到保存在堆内存中的对象。如果两个变量保存的是同一个地址值,说明指向的是同一份数据,当一个变量修改属性时,另一个也必然会受到影响。
三、网络
1. get请求和post请求的区别?
- get请求的参数是拼接在地址栏中,隐私性和安全性都比较差,请求的数据长度是有限制的,
不同的浏览器和服务器不同,一般限制在 2~8K 之间,更加常见的是 1k 以内;post请求是没有的长度限制的,请求数据是放在body中。 - get请求一般是去取获取数据(也可以提交,但常见的是获取数据);post请求一般是去提交数据。
- get请求刷新服务器或者回退没有影响,post请求回退时会重新提交数据请求。
- get请求可以被缓存,post请求不会被缓存。
- get请求会被保存在浏览器历史记录当中,post不会。get请求可以被收藏为书签,因为参数就在url中,但post不能。post请求的参数不在url中。
- get请求只能进行url编码(appliacation-x-www-form-urlencoded),post请求支持多种(multipart/form-data等)。
2. get和post是否使用缓存?
- get和post是http协议中两种异曲同工的请求方式,http协议是基于TCP/IP的应用层协议,所以无论是get还是post用的都是同一个传输层协议,所以在传输上可以认为基本无差。
- get请求类似于查找的过程,通常是向服务器获取数据时使用,比如查询,get请求如果带参数,参数会拼接在地址栏的url中暴露出来,http缓存通常只适用于不改变服务端数据的请求,所以get符合http缓存,适用于不改变服务端请求数据的这个原则,所以说get请求可以被缓存。
- post不同,post一般向服务器提交数据时使用,做的是修改和删除工作,比如添加和修改表单,post参数会放在请求体中,所以必须与数据库交互,所以不能使用缓存。
3. 阿帕奇和Nginx如何配置?
阿帕奇(Apache)和 Nginx 都是常用的 Web 服务器软件,它们可以用来处理 HTTP 请求并向客户端发送响应。下面是它们的配置方法简述:
阿帕奇配置:
1. 修改 Apache 的配置文件 httpd.conf(或 .htaccess 文件),配置虚拟主机和监听端口等参数。
2. 配置 Apache 的模块,如 PHP 模块,通过修改 httpd.conf 文件或添加 .conf 文件。
3. 重启 Apache 服务器,使配置生效。
Nginx 配置:
1. 修改 Nginx 的配置文件 nginx.conf,配置虚拟主机、监听端口、反向代理等参数。
2. 配置 Nginx 的模块,如 Lua 模块、gzip 模块等,通过在 nginx.conf 文件中添加配置项。
3. 重启 Nginx 服务器,使配置生效。
需要注意的是,在配置阿帕奇和 Nginx 的过程中,需要根据实际情况选择不同的配置选项,并确保配置文件的正确性和安全性。另外,由于 Nginx 的性能优于阿帕奇,因此在高并发的情况下,可以考虑使用 Nginx 作为 Web 服务器。
4. 前后端交互?
前后端交互是指前端页面和后端服务器之间的数据传输和通信。前端通常使用 JavaScript 发送 HTTP 请求(如 GET、POST 请求等),后端服务器接收请求并处理请求,返回相应的数据给前端。
下面是前后端交互的基本流程:
1. 前端页面通过 JavaScript 构造 HTTP 请求,包括请求方法(GET、POST 等)、请求 URL、请求头、请求体等。
发送 HTTP 请求到后端服务器。可以使用浏览器的 XMLHttpRequest 对象、Fetch API,或者第三方库(如 axios)发送请求。
2. 后端服务器接收请求,根据请求 URL 和请求方法进行处理,并从数据库或其他资源中获取数据,对数据进行处理和计算。
后端服务器将处理后的数据封装成 HTTP 响应,包括响应状态码、响应头、响应体等。
3. 前端页面接收到 HTTP 响应,解析响应数据,并根据数据更新页面内容。
4. 需要注意的是,前后端交互需要遵循一定的协议和规范,例如 HTTP 协议、JSON 数据格式等。此外,为了提高交互效率和用户体验,可以使用一些技术和工具,如 Ajax、WebSocket、RESTful API 等。
5. 前后端如何联调?
- 前端和后端分离,由不同人员负责开发的时候,在开发过程中前后端联调进行数据对接与调试就是开发中经常会遇到的事情。我们平时练习前端开发的时候,是直接提供接口文档和服务器地址,然后我们直接在webpack中配置就可以请求到数据了。但是在实际开发中,服务器地址有了,但是后端的接口还没有写完,还没有部署到共用的服务器上。
- 开发前,和后端约定好接口协议,数据格式。(前端需要显示的数据内容和格式一般json比较多)
- 开发中,前端通过mock数据,完成本地开发和ui渲染
- 联调:后端给前端接口文档,前端可以通过postman调试,也可以把接口放在前端项目中调试,请求返回的接口数据有疑问就继续让后端调整就行。
通俗说就是前端去调后端的接口,把数据传给后端或者从后端那边取数据。作为前端,我们主要关注的是接口的地址是什么,前端调接口的时候要传什么参数给后端,后端又会返回什么样的数据给我们等等。这些信息我们都会从接口文档上找到答案, 接口文档一般由后端按规范编写后提供给我们,在开发中,如果发现实际收到的数据和接口文档上不一致,那么这时候就可以和后端进行沟通,前端不能擅自修改,如果有的数据格式,不是我们想要的无非也就两种情况,要么是和后端老师沟通让后台老师改,要么前端自己把数据处理成理想的状态。
6. 浏览器的缓存机制?
为什么需要浏览器缓存?
1. 浏览器的缓存,主要针对的是前端的静态资源。最好的效果是,我们在发起请求后,拉去相应的静态资源,并且保存在本地。如果服务器的静态资源没有更新,那么在下次请求的时候,就直接从本地读取;如果服务器的静态资源已经更新,那么我们再次请求的时候,就到服务器拉取新的资源,并保存在本地。这样能减少请求的次数,提高网站的性能。
2. 所谓的浏览器缓存:指的是浏览器将用户请求过的静态资源,存储到电脑本地磁盘中,当浏览器再次访问的时候,就可以直接从本地加载,不需要再去服务端请求了。
3. 使用浏览器缓存,有以下优点:
(1)减少服务器的负担,提高了网站的性能。
(2)加快客户端请求网页的加载速度。
(3)减少了多余网络数据传输。
7. 在地址栏里输入一个地址回车会发生那些事情?
(1) 解析URL
(2) 缓存判断
(3) DNS解析
(4) 获取MAC地址
(5) TCP三次握手
(6) HTTPS握手
(7) 返回数据
(8) 页面渲染
(9) TCP四次挥手
8. HTTP和HTTPS协议的区别是什么?
- http,也就是超文本传输协议,是互联网上应用最为广泛的一种传输协议,它是以明文方式发送信息的,所以如果有不法分子截取了Web浏览器和服务器之间的传输报文,就可以直接获得信息,可想而知这样是不安全。
- https可以认为是http的安全版,是在HTTP的基础上加入了SSL协议,https中的s就指的是SSL协议,这一层协议加在了传输层和应用层之间,它可以对通信数据进行加密,且能够验证身份,使得通信更加安全。当然不是绝对安全,只是会增加不法分子的破坏成本。
- https和http的区别:
(1)二者的连接方式不同,https的端口是443,而http的端口是80。
(2)http传输是明文的,HTTP协议不适合传输一些敏感信息,比如:信用卡号、密码等支付信息。而https是用ssl进行加密的,安全性更高。
(3)https要申请ca证书,一般免费证书较少,所以需要一定的费用,而http不需。
(4) http协议速度比https更快,因为较https而言,它不需要经过复杂的ssl握手。而https协议需要经过一些复杂的安全过程,页面响应速度会来得慢。
9. tcp和udp的区别及应用场景
tcp和udp两者都是通信协议,也都是传输层协议,但是他们的通信机制和应用场景不同。
- udp是无连接的(发送数据前不需要建立连接)tcp是面向连接的(需要建立连接后传输数据,这里说的就是tcp的三次微信公众号[编程十点半]握手);
- udp是不可靠传输,tcp是可靠传输(因为tcp拥有拥塞控制等机制,能够保证数据无差错传递,而udp没有相关机制,不保证数据的可靠交付)
- udp是面向报文传输,tcp将数据看成一串无结构的字节流,是面向字节流传输的。
- udp 支持一对一、一对多、多对多的交互通信;tcp 是一对一的两点服务,即一条连接只有两个端点。综上,可知tcp 是可靠的传输协议,但效率慢;而udp是不可靠的传输协议,但效率快。
因此,tcp的适用场景:用于对通信数据的完整性和准确性要求较高的情况。如: 重要文件传输,邮件发送等。
udp的适用场景:用于对通讯速度要求较高,对数据信息的安全性和完整性要求相对较低的情况。如: 网络电话、视频会议直播等实时通信。
10. TCP的三次握手、四次挥手?
- 首先我们要知道建立连接的目的是什么,我们是为了可靠的传输数据。那既然是可靠的传输数据,我们必须保证客户端和服务端都能正常的发送和接收数据,如果某一方不能正常的发送或者接收数据,那整个数据的传输就不能成功,也就不可靠。
三次握手
- 第一次握手:第一次握手是客户端发送同步报文到服务端,这个时候客户端是知道自己具备发送数据的能力的,但是不知道服务端是否有接收和发送数据的能力;
- 第二次握手:当服务端接收到同步报文后,回复确认同步报文,此时服务端是知道客户端具有发送报文的能力,并且知道自己具有接收和发送数据的能力,但是并不知道客户端是否有接收数据的能力;
- 第三次握手:当客户端收到服务端的确认报文后,知道服务端具备接收和发送数据的能力,但是此时服务端并不知道自己具有接收的能力,所以还需要发送一个确认报文,告知服务端自己是具有接收能力的。
最后,当整个三次握手结束过后,客户端和服务端都知道自己和对方具备发送和接收数据的能力,随后整个连接建立就完成了,可以进行后续数据的传输了。
四次挥手
- 第一次挥手客户端发起关闭连接的请求给服务端;
- 第二次挥手:服务端收到关闭请求的时候可能这个时候数据还没发送完,所以服务端会先回复一个确认报文,表示自己知道客户端想要关闭连接了,但是因为数据还没传输完,所以还需要等待;
- 第三次挥手:当数据传输完了,服务端会主动发送一个 FIN 报文,告诉客户端,表示数据已经发送完了,服务端这边准备关闭连接了。
- 第四次挥手:当客户端收到服务端的 FIN 报文过后,会回复一个 ACK 报文,告诉服务端自己知道了,再等待一会就关闭连接。
为什么握手要三次,挥手却要四次?
- 因为握手的时候并没有数据传输,所以服务端的 SYN 和 ACK 报文可以一起发送,但是挥手的时候有数据在传输,所以 ACK 和 FIN 报文不能同时发送,需要分两步,所以会比握手多一步。
为什么客户端在第四次挥手后还会等待 2MSL?
- 等待 2MSL 是因为保证服务端接收到了 ACK 报文,因为网络是复杂了,很有可能 ACK 报文丢失了,如果服务端没接收到 ACK 报文的话,会重新发送 FIN 报文,只有当客户端等待了 2MSL 都没有收到重发的 FIN 报文时就表示服务端是正常收到了 ACK 报文,那么这个时候客户端就可以关闭了。
11. 水平越权和垂直越权?
- 首先解释下什么是越权访问漏洞:越权漏洞是一种很常见的逻辑安全漏洞。是某应用在检查授权的时候存在纰漏问题,是由于服务器端对客户提出的数据操作请求过分信任,忽略了对该用户操作权限的判定,导致修改相关参数就可以拥有了其他账户的增、删、查、改功能,从而导致越权漏洞。
- 水平越权指的是同一权限下的不同用户可以互相访问。攻击者尝试访问与他拥有相同权限的用户的资源,怎么理解呢?比如某系统中有个人资料这个功能,A账号和B账号都可以访问这个功能,但是A账号的个人信息和B账号的个人信息不同,可以理解为A账号和B账号个人资料这个功能上具备水平权限的划分。此时, A账号通过攻击手段访问了B账号的个人资料,这就是水平越权漏洞。
水平越权常见场景
1、基于用户身份的ID
在使用某个功能时通过用户提交的身份ID (用户ID、账号、手机号、证件号等用户唯一标识)来访问或操作对应的数据。
2、基于对象ID
在使用某个功能时通过用户提交的对象ID (如订单号、记录号)来访问或操作对应的数据。
3、基于文件名
在使用某个功能时通过文件名直接访问文件,最常见于用户上传文件的场景。
- 垂直越权,权限低的用户可以访问到权限高的用户。
垂直越权是不同级别之间或不同角色之间的越权,垂直越权还可以分为向上越权和向下越权。向上越权指的是一个低级别用户尝试访问高级别用户的资源,比如说某个系统分为普通用户和管理员用户,管理员有系统管理功能,而普通用户没有,那我们就可以理解成管理功能具备垂直权限划分,如果普通用户能利用某种攻击手段访问到管理功能,那我们就称之为向上越权(就是以下犯上)。向下越权是一个高级别用户访问低级别用户信息(那这也是不行的,我们每个人都要有私生活和小秘密)。
垂直越权常见场景
1、未认证账户访问无需认证就能访问该功能
2、不具备某个功能权限的账户认证后成功访问该功能
12. 前端常见的安全问题?
在web安全领域中,XXS和CSRF是最常见的两种攻击方式。
XXS的英文全称是Cross Site Script,中文翻译过来是跨站脚本攻击。XSS(跨站脚本攻击)是指攻击者在返回的HTML中嵌入javascript脚本,为了防御XXS攻击,需要在HTTP头部配上,set-cookie:http-only ;这个属性使得脚本无法获取,它会禁止javascript脚本来访问cookie。也可以使用验证码登陆,这样避免脚本伪装成用户执行一些操作。
- 攻击者对客户端网页注入的恶意脚本一般包括 JavaScript,有时也会包含 HTML 和 Flash。有很多种方式进行 XSS 攻击,但它们的共同点为:将一些隐私数据像 cookie、session 发送给攻击者,将受害者重定向到一个由攻击者控制的网站,在受害者的机器上进行一些恶意操作。
- XSS攻击可以分为3类:存储型(持久型)、反射型(非持久型)、基于DOM。
CSRF的英文全称是Cross-site request forgery,所以又称为“跨站请求伪造”,是指黑客引诱用户打开黑客的网站,在黑客的网站中,利用用户的登录状态发起的跨站请求。简单来讲,CSRF攻击就是黑客利用了用户的登录状态,并通过第三方的站点来做一些坏事。
详细理解转: https://juejin/post/6945277278347591688
13. localStorage、sessionStorage、cookie?
共同点
- SessionStorage, LocalStorage, Cookie三者都可以被用来在浏览器端存储数据,而且都是字符串类型的键值对。 区别在于,前两者属于WebStorage,创建它们的目的便于客户端存储数据。 而Cookie早在网景公司的浏览器中就开始支持,最初目的是为了保持HTTP的状态。
区别
- 存储大小上的区别
cookie:一般不超过4K(因为每次http请求都会携带cookie、所以cookie只适合保存很小的数据,如会话标识)
localStorage和sessionStorage:5M或者更大 - 数据的有效时间不同
cookie : 设置的cookie过期时间之前一直有效,存放在硬盘里,过期才失效即使窗口或浏览器关闭;
localStorage : 永久存储,浏览器关闭后数据不丢失,除非手动永久清除,因此用作持久数据;
sessionStorage :生命周期是数据在当前浏览器窗口关闭后自动删除。 - 数据与服务器之间的交互方式不同
cookie的数据会自动的传递到服务器,服务器端也可以写cookie到客户端;
sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存; - 易用性上的差别
cookie :源生的cookie接口不友好 ,需要程序员自己封装。
sessionStorage和localStorage源生接口可以接受也可再次封装来对Object和Array有更好的支持。 - 应用场景
cookie:判断用户是否登录过网站,以便实现下次自动登录或记住密码;保存事件信息等
localStorage:常用于长期登录(判断用户是否已登录),适合长期保存在本地的数据
14. 浏览器缓存策略?强缓存(本地缓存)和协商缓存(弱缓存)?
- 浏览器在发送请求之前会先检查强缓存,如果没有需要的内容,浏览器就会向服务器发起请求,判断是否需要弱缓存。
- 根据是否需要向服务器重新发起HTTP请求,将缓存过程分为两个部分:强缓存和协商缓存。
- 强缓存,浏览器发送请求前,会先去缓存里查看是否命中强缓存,如果命中,则直接从缓存中读取资源,不会发送请求到服务器。浏览器是不发起请求的,直接使用缓存里的内容。浏览器会把js、css、html、image等存在内存中,下次用户访问的时候,直接去缓存里取就可以了,提高性能。强缓存的触发:HTTP1.0: 时间戳响应标头;HTTP1.1: Cache-Control响应标头。
- 协商缓存。当强缓存没有命中时,浏览器一定会向服务器发起请求。服务器会根据 Request Header 中的一些字段来判断是否命中协商缓存。如果命中,服务器会返回 304 响应,但是不会携带任何响应实体,只是告诉浏览器可以直接从浏览器缓存中获取这个资源。如果本地缓存和协商缓存都没有命中,则从直接从服务器加载资源。协商缓存的触发:HTTP1.0: 请求头:if-modified-since ,响应头:last-modified。HTTP1.1:请求头:if-none-match,响应头: ETag。
15. token在项目中是用来干什么的?为什么请求要携带token?
- token是令牌的意思,客户第一次登陆时,服务器会生成一段加密的字符串,然后会返回给客户端。后面客户端每次向服务端请求资源的时候,只需要带着token,不需要带着用户名和密码去请求。
- 为什么要带token?是因为用户登录成功之后,后续还会登陆去服务器获取数据。服务器对每一次前端的请求都要去验证是哪一位用户发送的,用户是否合法,这样反复去查询数据库,会给服务器造成压力,当后续请求都带上token后,服务器直接解密token,就知道了用户的相关信息,省去了查询数据库的操作,减轻了数据库的压力。这就是token的作用。又因为基本的所有请求都要携带token,总不能每一次都要手动配置,所以我们可以在请求拦截器中统一封装,让每一次请求都能带上token。
16. 你平常做项目的时候,遇到过哪些安全漏洞?
(1)form表单提交前加上校验,防止xss攻击。为防止xss攻击,表单的每个字段提交需要做校验或者编码过滤。校验的话可以用正则,比如校验手机号或者邮箱之类的。编码过滤的话,提交前需要对提交的内容进行编码过滤,防止特殊的标签之类的提交到后台。比如用户输入 '’ 这类的脚本或者html标签之类的。要过滤掉,防止提交到后台。
(2)限制URL访问,越权访问。1在公共模块增加校验方式,查看是否具有对应权限。例如,每个客户只能查看和修改自己的信息,在url地址栏参数中,带的参数有序列号之类的,攻击者可能会想到,客户的序列号是按照顺序往下排的,要是按顺序加一减一是不是就可能访问到别人的账号(水平越权)。还有一种是不同级别的登陆者登陆所拥有的功能权限不同,低权限者可能访问高权限者的账号,从而使用原本它不具有的功能,这种也是越权漏洞,属于垂直越权。2监听路由跳转,在路由跳转之前,增加校验(路由导航守卫)。3和后台联调,将对应的信息存入cookie,在数据访问时进行对比。
(3)文件上传漏洞。例如用户上传任意类型的文件可能会让攻击者注入危险内容或恶意代码,并在服务器上运行。解决:1严格限制用户上传的文件后缀以及文件类型。2定义上传文件类型白名单,只允许白名单里面类型的文件上传。3文件上传目录禁止执行脚本解析,避免攻击者进行二次攻击。
17. 什么是nodejs技术?
- nodejs是基于chrome V8引擎的js运行环境(一个软件,可以运行js代码)。
- Nodejs实际上就是一种在服务器环境运行js的平台,因为它 封装了谷歌的v8引擎,使其能够运行js。
(js: 运行在浏览器,开发前端程序。Nodejs: 运行在服务器,开发后端程序。)
四、vue面试题
1. vue2双向数据绑定的原理是什么?
- 举一个双向数据绑定的例子,当你在输入框输入文字的时候,vue会检测到数据的变化,然后更新对应的视图。反过来也一样,如果你通过代码修改了数据,vue也会自动更新视图。
- 双向数据绑定的原理是通过数据劫持 和 发布订阅模式实现的。首先vue通过Object.defineProperty() 方法对数据进行劫持,在数据变动的时候进行拦截,调用getter和setter方法。其次,当监听到数据变动时,vue就会触发发布订阅模式,vue会通知所有的订阅者进行更新。因此,当用户在页面上进行修改的时候,vue会更新对应的数据,并通知所有订阅者更新视图;同时,当数据发生改变的时候,vue也会更新对应的视图。通过这样的机制,vue实现了双向数据绑定,使得数据和视图的变化可以互相影响。
- 补充:订阅者是vue中的一个概念,它是用于管理更新视图的对象,当数据发生变化时,vue会通知所有的订阅者进行更新。发布者就是变动的数据,订阅者就是在页面中使用到该变量的地方,对此进行数据更新。【一个发布者多个订阅者】。
- vue3中用ES6的proxy对象替换了Object.defineProperty(),因为Object.defineProperty()只能劫持一个属性,而proxy可以劫持对象的所有属性
2. vue2和vue3的区别?
- vue2和vue3双向数据绑定原理发生了改变。
vue2 的双向数据绑定是利用ES5 的一个 API Object.definePropert()对数据进行劫持 ,结合发布订阅模式的方式来实现的。vue3 中使用了 es6 的 ProxyAPI 对数据代理。 - Vue3支持碎片(Fragments),就是说在组件中可以拥有多个根节点。
- Vue2与Vue3 最大的区别 — Vue2使用选项类型API,Vue3合成型API。旧的选项型API在代码里分割了不同的属性: data,computed属性,methods,等等。新的合成型API能让我们用方法(function)stup(){}来分割,相比于旧的API使用属性来分组,这样代码会更加简便和整洁。
- 建立数据 data。Vue2把数据放在data中,在Vue3中,我们就需要使用一个新的setup()方法,此方法在组件初始化构造的时候触发。
- 使用以下三步来建立反应性数据:
- 从vue引入reactive
- 使用reactive()方法来声名我们的数据为响应性数据
- 使用setup()方法来返回我们的响应性数据,从而我们的template可以获取这些响应性数据
3. 什么是虚拟DOM?
- 在Vue中,虚拟DOM(Virtual DOM)是一个轻量级的JavaScript对象,用于描述真实DOM的层次结构和属性。每当Vue组件的数据发生变化时,Vue会先对虚拟DOM进行操作,然后再将变化的部分同步到真实DOM中,从而避免了直接操作真实DOM时的性能问题。
- 虚拟DOM的出现是为了解决传统前端开发中频繁操作真实DOM的问题。由于真实DOM的操作往往非常消耗性能,因此频繁操作会导致页面的性能问题。而虚拟DOM可以在内存中对DOM进行操作,只有在必要的时候才将变化同步到真实DOM,从而避免了频繁操作真实DOM的性能问题。
- 具体来说,当Vue组件的数据发生变化时,Vue会通过比较新旧虚拟DOM树的差异来确定需要更新的部分,并将这些部分同步到真实DOM中。这样就避免了不必要的DOM操作,从而提高了页面的性能和响应速度。
- 此外,虚拟DOM还可以方便地实现一些高级特性,例如组件的复用和动画效果。虚拟DOM使得Vue可以在不操作真实DOM的情况下实现这些功能,从而提高了开发效率。
4. vue 组件之间的通信
1、父传子:父组件通过自定义属性传递给子组件,子组件中通过props接收父组件中的绑定的属性
2、子传父:子组件通过广播的方式
e
m
i
t
发送自定义事件,将值传递给父组件,父组件监听事件,触发一个函数去接收子组件中传递过来的值。
3
、兄弟间传值:
(
1
)
通过父组件中转来传值,即
A
和
B
是兄弟组件,可以
A
传给父组件,由父组件再传给
B
(
2
)
n
e
w
一个
B
u
s
实例,在需要发送数据的组件中自定义方法,通过
emit发送自定义事件,将值传递给父组件,父组件监听事件,触发一个函数去接收子组件中传递过来的值。 3、兄弟间传值: (1)通过父组件中转来传值,即A和B是兄弟组件,可以A传给父组件,由父组件再传给B (2) new一个Bus实例,在需要发送数据的组件中自定义方法,通过
emit发送自定义事件,将值传递给父组件,父组件监听事件,触发一个函数去接收子组件中传递过来的值。3、兄弟间传值:(1)通过父组件中转来传值,即A和B是兄弟组件,可以A传给父组件,由父组件再传给B(2)new一个Bus实例,在需要发送数据的组件中自定义方法,通过emit传递数据,在需要接收数据的组件生命周期created中,通过$on监听获取数据。
(3) 使用vuex状态管理,可以实现数据的随意存储和获取。
5. vuex状态管理工具的五个核心属性?
- state: 用来存储公共数据的(变量),类似于组件中的data。
- mutations:数据修改的逻辑,也是唯一修改state数据的地方。(提交更新数据的方法),它必须是同步操作,如果有异步操作的话,那么就需要actions。
- actions:它也是用来改变数据的,但是它无法直接修改数据,actions提交的是mutations,在mutations里面更改数据,actions支持异步操作。
- getters:从基本数据,派生过来的数据,相当于组件里的计算属性computed。
- modules:是用来模块化vuex的,可以让每一个模块拥有自己的state、mutation、actions、getters使得结构更加清晰,方便管理。
6. vue子组件和父组件执行的顺序?
加载渲染过程:
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
子组件更新过程:
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
父组件更新过程:
父 beforeUpdate -> 父 updated
销毁过程:
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
7. MVVM框架是什么?
- MVVM是Model-view-ViewModel的简写,Model是模型,View视图,ViewModel是视图模型,连接view和model的桥梁。
- 要实现一个observer观察者,当数据发生变化,ViewModel能够检听到数据的变化,然后通知到对应的视图做自动更新;当用户操作视图,ViewModel也能监听到视图的变化,然后通知数据做改动,这实际上就实现了数据的双向绑定。
8. 为什么说vue是一个渐进式框架,你是怎么理解的?
- 当我们开始使用渐进式框架的时候,我们可以只使用这个框架的某部分或者部分功能,而不是立即全面的采用整个框架。也就是说,我们可以根据自己的需求和项目的特点,去逐步应用框架的功能。
- 这种方式。可以使得采用新框架的方式更加平滑,降低学习成本,同时也能更好地控制项目的开发进度。
- 在项目中,我们可以逐渐得使用模板语法、组件系统、vuex状态管理、路由等不同的特性,如果不需要某些特性,我们就不去使用它,这就使用起来就很灵活。
- 例如。vue得状态管理工具vux,如果我们的项目比较简单,我们就可以不使用它,这就是vue渐进式框架开发的理念。就是在vue中你想用什么就用,不想用就不用。
9. 如何实现图片懒加载?
- 懒加载是一种在页面加载时,延迟加载一些非关键资源的技术,换句话说,就是按需要加载。当我们碰到长网页有很多图片时,我们先加载出现在视口内的几张图片,当滚动条滚动到对应图片的位置时,再去加载别的图片。这种延迟加载的方式就是懒加载。(卷出去的高度 + 屏幕的高度 = 文档的高度)
- 图片懒加载是怎么实现的?就是我们先设置图片的data-set属性 (当然也可以是其他任意的,只要不会发送http请求就行了,作用就是为了存取值)值为其图片路径,由于不是src,所以不会发送http请求。然后我们计算出页面scrolITop的高度和浏览器的高度之和,如果图片距离页面顶端的坐标Y(相对于整个页面,而不是浏览器窗口)小于前两者之和,就说明图片就要显示出来了 (合适的时机,当然也可以是其他情况) ,这时候我们再将 data-set 属性替换为 src 属性即可。
- vue中使用vue异步组件 和 ES中的import实现懒加载。
异步组件使用路由懒加载,方法如下:component:resolve=>(require([‘需要加载的路由的地址’]),resolve)
// 代码如下:
import Vue from 'vue'
import Router from 'vue-router'
/* 此处省去之前导入的HelloWorld模块 */
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: resolve=>(require(["@/components/HelloWorld"],resolve))
}
]
})
- ES 提出的import方法,(----最常用----)
方法如下:const HelloWorld = ()=>import('需要加载的模块地址')(不加 { } ,表示直接return)
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
const HelloWorld = ()=>import("@/components/HelloWorld")
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component:HelloWorld
}
]
})
10. 为什么token要同时存在vuex和localStorage中?
- vuex存储数据的特点:数据统一全局管理,一旦数据在某组件更新,其他所有组件数据都会更新,也就是说它是响应式的,但是如果数据只存在vuex中,刷新页面vuex里的数据会重新初始化,导致数据丢失,恢复到原来的状态。
- localStorage(本地存储)存储数据的特点:永久性存储,但不是响应式的,当某个组件数据修改时,其他组件无法同步更新。
- 另外,vuex是存储在内存中,localStorage本地存储是存储到磁盘里,从内存中读取数据,速度是远高于磁盘的,所以把数据存在vuex中可以提高获取token速度,提高性能。
- 结论: 所以我们在项目中通常是结合这两者使用,拿到token后,把token存储到localStorage和vuex中,vuex保证数据在各组件间同步更新,如果刷新页面数据丢失,我们可以从localStorage获取,通过结合vuex和localStorage本地存储,实现数据的持久化。
11. vue中请求到底写在created中还是mounted中?
- created:中文意思创建完成,这时候已经初始化了某些属性值,Vue实例中的 data和methods已经可以使用了。但是,还没有挂载到页面上。
- mounted: 中文意思挂载完成,这时候初始化页面完成,此时页面已经渲染出来了,可以进行dom操作。
在实际开发中请求不论放在created还是mounted大多时候是没有区别的,因为created和mounted都是同步的,而请求是异步的,不会堵塞页面渲染的主线程,我们也不能控制请求回来的时间。主要是看个人习惯吧。
但是如果是需要操作dom相关的请求,就要在mounted中执行,因为这时候页面才挂载完成,才可以进行dom操作。
另外需要补充一点,官方文档上给大家提的一个醒,就是mounted阶段不保证所有的子组件也都被挂载完成,这时候如果我们希望等到整个视图都渲染完毕再做操作,那就需要使用到this.$nextTick方法。
12. 后端返回十万条数据,前端怎么处理?
1. 触底加载
- 只要滚动一次就要判断一次加载时机,当滚动上去的高度 + 屏幕的高度 >= 页面的高度,需要加载下一页数据。
我们需要获取到滚动上去的高度,窗口的高度,文档的高度。
获取页面滚动上去的高度:document.documentElement.scrollTop/document.body.scrollTop
获取当前元素的宽度和高度:ele.offsetHeight/ele.offsetWidth
获取窗口的宽度和高度:window.innerHeight
获取文档的宽度和高度:document.documentElement.scrollHeight
2. 虚拟列表
- 由于最终效果需要是一个长列表的形式,那么常规的分页渲染,显然是不符合要求的。这个时候我们可以考虑用虚拟列表来实现需求。
- 什么是虚拟列表?
虚拟列表就是只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染,以实现减少消耗,提高用户体验的技术。它是长列表的一种优化方案,性能良好。 - 实现思路:
- (1)写一个代表可视区域的div,固定其高度,通过overflow使其允许纵向 Y 轴滚动。
- (2)第二步,计算区域中可以显示的数据条数。这个可以用可视区域的高度除以单条数据高度得到。
- (3)监听滚动,当滚动条变化时,计算出被卷起的数据的高度。
- (4)计算区域内数据的起始索引,也就是区域内的第一条数据:这个用卷起的高度除以单条数据高度可以拿到。
- (5)计算区域内数据的结束索引。通过起始索引+可显示的数据的条数可以拿到。
- (6)取起始索引和结束索引中间的数据,渲染到可视区域。
- (7)计算起始索引对应的数据在整个列表中的偏移位置并设置到列表上。
- 整个步骤下来,最终的效果是:不论怎么滚动,我们改变的只是滚动条的高度和可视区的元素内容。每次只会渲染一个固定的条数,不会增加多余元素。
<template>
<div :style="{height: `${contentHeight}px`}" class="content_box" @scroll="scroll">
<!--这层div是为了把高度撑开,让滚动条出现,height值为所有数据总高-->
<div :style="{'height': `${itemHeight*(listAll.length)}px`, 'position': 'relative'}">
<!--可视区域里所有数据的渲染区域-->
<div :style="{'position': 'absolute', 'top': `${top}px`}">
<!--单条数据渲染区域-->
<div v-for="(item,index) in showList" :key="index" class="item">
{{item}}
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "list",
data(){
return{
listAll: [], //所有数据
showList: [], //可视区域显示的数据
contentHeight: 500, //可视区域高度
itemHeight: 30, //每条数据所占高度
showNum: 0, //可是区域显示的最大条数
top: 0, //偏移量
scrollTop: 0, //卷起的高度
startIndex: 0, //可视区域第一条数据的索引
endIndex: 0, //可视区域最后一条数据后面那条数据的的索引,因为后面要用slice(start,end)方法取需要的数据,但是slice规定end对应数据不包含在里面
}
},
methods:{
//构造10万条数据
getList(){
for(let i=0;i<100000;i++){
this.listAll.push(`我是第${i}条数据呀`)
}
},
//计算可视区域数据
getShowList(){
this.showNum = Math.ceil(this.contentHeight/this.itemHeight); //可视区域最多出现的数据条数,值是小数的话往上取整,因为极端情况是第一条和最后一条都只显示一部分
this.startIndex = Math.floor(this.scrollTop/this.itemHeight); //可视区域第一条数据的索引
this.endIndex = this.startIndex + this.showNum; //可视区域最后一条数据的后面那条数据的索引
this.showList = this.listAll.slice(this.startIndex, this.endIndex) //可视区域显示的数据,即最后要渲染的数据。实际的数据索引是从this.startIndex到this.endIndex-1
const offsetY = this.scrollTop - (this.scrollTop % this.itemHeight); //在这需要获得一个可以被itemHeight整除的数来作为item的偏移量,这样随机滑动时第一条数据都是完整显示的
this.top = offsetY;
},
//监听滚动事件,实时计算scrollTop
scroll(){
this.scrollTop = document.querySelector('.content_box').scrollTop; //element.scrollTop方法可以获取到卷起的高度
this.getShowList();
}
},
mounted() {
this.getList();
this.scroll();
}
}
</script>
<style scoped>
.content_box{
overflow: auto; /*只有这行代码写了,内容超出高度才会出现滚动条*/
width: 700px;
border: 1px solid red;
}
/*每条数据的样式*/
.item{
height:30px;
padding: 5px;
color: #666;
box-sizing: border-box;
}
</style>
13. 插槽是什么?插槽如何使用?
- Vue实现了一套内容分发的API,将元素作为承载分发内容的出口。
- 通过插槽可以动态指定某一个组件模板部分的渲染, 我们在调用组件的时候, 在组件的调用标签中间传递了什么样的标签结构, 那么该组件就会把我们传递的标签结构放在他的模板部分进行渲染。
- 举一个例子,如果你在一个自定义组件标签中又加入了一段HTML,默认情况下它里面的DOM元素是不会渲染出来的,但是如果你在子组件模板中写上插槽的话,这个标签内容会自动放在你写的插槽标签那个位置。
- vue的slot主要分三种:默认插槽,具名插槽,作用域插槽。
- 具名插槽:有名字的插槽,如果一个组件中有多个插槽,就可以写成具名插槽,给插槽提供的内容放在对应名字的的位置上。
<div id="app">
// 子组件
<Child>
// 给插槽提供的内容
<template v-slot:default>
<button>按钮</button>
<a href="https://huawei">跳转华为</a>
</template>
<template v-slot:header>
<h1>标题</h1>
<p>内容,21231215456454</p>
</template>
</Child>
</div>
<Child>子组件的模板中:
<div>
<h1>这是子组件的内容</h1>
<slot></slot> // 给插槽提供的内容将会被放在这个位置,这是默认插槽
</div>
- 使用插槽是在存在父子关系的组件,可以在子组件中决定插槽的位置,同时子组件也可以给这些插槽的默认信息,当父组件中没有需要给子组件插槽插入信息时,显示的是子组件插槽定义的默认信息。
14. 什么是SPA?
- SPA(Single Page Application), 单页面应用程序, 使用vue, react, angular ,创建的项目都属于 SPA。
- 单页面应用,只在web页面初始化时,加载相应的HTML、js、css,一旦页面加载完成,就不会因为用户的操作而进行页面的重新加载或者跳转。简单说,SPA它只有一个web页面,例如:vue项目只有一个index.html,但是我们为什么我们能看到不同的页面呢,这是因为vue的路由机制,通过监听路由的变化,实现HTML内容的变换,从而动态实现UI与用户的交互,就像我们在vue项目中,从一个菜单项切换到另外一个菜单项,页面的内容虽然变了,但是并没有去请求一个新的html的动作,而是通过变化的路由,去找到当前路由对应的页面。
- SPA的优点:1.由于页面初始化的时候,项目依赖的资源就统一加载了,所以后面切换页面就不用再向服务器请求,因此,切换速度快且流畅,用户体验性会更好,在一定程度上也会减小服务器的压力。2.前后端职责更加的清晰,前端就负责页面相关,以及调后端接口拿数据的工作;后端则负责数据相关的处理。
- SPA的缺点:首屏加载慢,因为是单页面应用,初次加载的时候资源会统一全部加载。当然,作为优化时的一种方案,部分页面也是可以按需加载的。另外,由于所有的内容都在一个页面中动态替换显示所以再SEO上有着天然的弱势,不利于搜索引擎优化。
15. 你怎么理解vue的单向数据流?
- 单项数据流是从上到下的,但是它不能从下到上。
- Vue中单向数据流指的是父组件可以传值给子组件,子组件不能直接修改父组件传的值。
- prop也就是父组件传过来的数据,如果我们试图通过子组件的v-model去改变这个prop,也就是试图通过子组件直接去改变父组件的数据,而不是通过发送事件的方式,这是不允许的。
16. 在什么阶段下才可以访问操作DOM?
- 调用mounted之前。
- 在钩子函数mounted被调用之前,Vue已经将编译好的模板挂载到页面上,所以在mounted中可以访问操作DOM。
17. 什么是微前端?
微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用可以独立运行、独立开发、独立部署。(建议先了解微服务)
(1)微服务就是一种架构风格
(2)微服务就是把一个项目拆分成独立的多个服务,并且多个服务是可以独立运行的,而每个服务都会占用线程。
18. 如何解决Jquery版本泄露问题?
正确解决方法:升级版本。
19. vue生命周期有哪些?
创建阶段: 只执行一次。
beforeCreate(开始进行一些数据和方法的初始化的操作, data 中的数据和 methods 中的方法还不能用)。
created(已经完成数据和方法的初始化, data 中的数据和 methods 中的方法可以使用了)。
挂载阶段:
beforeMount(开始渲染虚拟 DOM)。
mounted(已经完成了虚拟 DOM 的渲染, 可以操作 DOM 了, 只执行一次)。
更新阶段: 执行多次
beforeUpdate(data 中的数据即将被更新, 会执行多次)。
updated(data 中的数据已经更新完毕, 会执行多次)。
销毁阶段: 只执行一次
beforeDestroy(vue 实例即将销毁, 此时 data 中的数据和 methods 中的方法依然处于可用状态)。
destroyed(vue 实例已经销毁, 此时 data 中的数据和 methods 中的方法已经不可用)。
20. computed和watch的区别?
- computed 是计算属性, 所依赖得数据发生改变,就会重新计算结果那么就需要用到computed。
- 最典型的例子就是购物车结算时候的总金额,就是依赖数量和单价来进行计算的;另外computed支持缓存,只有依赖的数据发生改变的时候,才会重新进行计算,否则,会直接从缓存中读取。使用的时候和data中的数据的使用方式基本上是一致的,而且计算属性它不支持异步,当computed内有异步操作时,是无效的,无法监听到数据的变化。 (计算属性的函数必须有返回值)
- watch 是监视器, 当一个数据的变化,会影响其他一个或多个数据的时候,就需要监听这个数据,watch不支持缓存,监听的数据发生变化就会触发相应的操作。重视过程。不用返回值,同步异步都可以。
- 另外,watch支持异步,而且我们监听的数据必须是data中声明过的数据,或者是从父组件中传递过来的props中的数据,另外,每个监听数据有两个可选的属性,分别是immediate 和 deep。immediate是组件加载是否立即触发回调函数执行,如果它的值是true,组件加载就会立即触发一次;如果是false,首次是不会执行这个监听逻辑的,只有当数据改变的时候才会监听。deep是深度监听,为了监听对象内部值的变化,适合用在复杂类型的数据中。(楼层导航,切换楼层时,监听楼层索引的变化,调整滚动条位置)
26. Data为什么是一个函数?
因为对象是一种引用数据类型,在内存中只有一份. 如果 data 的值直接是一个对象的话, 那么后期组件在不同的地方多次调用的时候, 会相互产生影响, 因为每一次调用操作的 data 对象是一样的。使用函数的方式返回对象, 可以保证组件的每一次调用都会创建一个新对象,这样组件的每一次调用不会相互产生影响。
27. v-if 和 v-show的区别
- 共同点是:v-if 和 v-show 都能实现元素的显示隐藏。
- 区别:1. v-show 只是简单的控制元素的 display 属性,而 v-if 才是条件渲染(条件为真,元素会被渲染,条件 为假,元素会被销毁);2. v-show 有更高的首次渲染开销,而 v-if 的首次渲染开销要小的多;3. v-if 有更高的切换开销,v-show 切换开销小;4. v-if 有配套的 v-else-if 和 v-else,而 v-show 没有。5. v-if 可以搭配 template 使用,而 v-show 不能。
28. v-show 与 v-if的使用场景
- v-if 与 v-show 都能控制dom元素在页面的显示。
- v-if 相比 v-show 开销更大(直接操作dom节点增加与删除),如果需要非常频繁地切换,则使用 v-show 较好。如果在运行时条件很少改变,则使用 v-if 比较好。
29. 为什么vue中循环遍历的时候,尽量不要用索引值作为动态绑定的key值?
- 在Vue中,循环遍历通常使用v-for指令来实现。v-for指令通常需要绑定一个key值,帮助Vue跟踪每个列表项的身份,以便在列表中发生变化时进行高效的更新。
- 使用索引值作为key值可能会导致一些问题。例如,如果您的列表项在循环过程中被重新排序或过滤,则可能会出现问题。由于索引值是按顺序分配的,因此如果您在列表的中间插入或删除项目,则所有后续项目的索引值都会发生更改,从而导致Vue进行不必要的重新渲染。
- 此外,使用索引值作为key值还可能导致性能问题。在处理大型列表时,Vue可能需要在每次更新时对整个列表进行重新渲染,这可能会导致性能瓶颈。
- 为了避免这些问题,建议使用列表项中具有唯一标识符的属性作为key值。这可以确保每个项具有唯一的身份,并且在列表中进行排序或过滤时仍然能够正确更新。如果您的列表项没有唯一标识符,则可以考虑创建一个。
30. nextTick
- 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
// 修改数据
vm.msg ='Hello'// DOM 还没有更新
Vue.nextTick(function(){
// DOM 更新了})
this.$nextTick()
31. 说一下keep-alive 标签的作用,以及它的使用场景?
- 在Vue中,keep-alive标签可以用来缓存组件,当一个组件被包裹在keep-alive标签中时,离开当前页面时,这个组件不会被销毁,而是被缓存起来,当这个组件再次被使用时,Vue会从缓存中提取组件实例,并重新挂载,而不是重新渲染。
- 这个功能可以提高应用的性能,特别是在需要频繁切换组件的场景下,就比如Tab切换或者路由切换,因为不需要每一次切换时都重新创建和销毁组件,而是直接从缓存中获取,这样可以避免重复的初始化和渲染,从而提高应用的响应速度和性能。
- 举个应用场景,有个员工列表,现在我们点击某条数据,查看员工详情后,再返回到员工列表,这个时候我们就希望这个列表能够保持刚才的状态,这时候就可以使用keep-alive把这个列表所在的组件包裹起来。
32. 组件化和模块化一样吗?
- 模块化是从代码逻辑角度进行划分的,保证每个模块的职能单一,比如登录页的登录功能,就是一个模块,注册功能又算是一个模块。
- 组件化,是从UI界面的角度划分的;页面上每个独立的区域,都可以视为一个组件,前端组件化开发,便于UI组件的复用,减少代码量。
- 区别是:划分角度不同,组件化是从UI界面角度来划分的,模块化是从代码逻辑角度来划分的。
33. data和props的区别?
- 在实际vue项目中,我们经常会在子组件里看到data和props属性,这两者里面的数据使用方式基本是一致的。但是还是有一定的区别。
- data不需要父组件传值,自身进行维护;而props需要父组件传值。
- data上的数据都是可读可写的;而 props的数据都是父组件传递过来的,而且由于它是单向数据流,因此数据只可读,无法重新修改。
- 如果传递的props值是基本类型(像Number,Boolean,String)子组件直接修改,控制台肯定会报错的,但是如果传递的是引用类型,像Object,数组结构,子组件修改里面的属性值或者某一数组元素,控制台是不会报错的。因为对于引用类型改的只是值,而不是引用地址。
- 不过,不管传递什么形式的数据,我们都是不建议在子组件中直接修改Props的值的,因为这样会破坏单一数据流,可能会导致数据的变化无法追踪。
问:那在子组件中修改props的正确操作又是什么呢?
- 答:如果子组件只是想修改后自己使用,不想影响到父组件的数据,那么我们可以在子组件中的data里定义一个变量,让这个变量的初始值等于父组件传过来的 props值,相当于copy一份这个props值,后面需要修改的话就改自己data里的这个值。这样就不会影响到父组件了。如果处理后想同步修改父组件的值,那么可以通过this.$emit事件触发父组件去修改。
34. 白屏时间和首屏时间的区别?
- 白屏时间(First Paint):是指用户输入内容回车,到浏览器开始出现第一个字符,即开始显示内容的时间。所以,白屏时间 = 页面开始展示的时间点 - 开始请求的时间点。
- 首屏时间(First Contentful Paint):是指浏览器从响应用户输入网络地址,到首屏内容渲染完成的时间。所以,首屏时间=首屏内容渲染结束时间点 - 开始请求的时间点。
- 通过刚刚的两个概念,我们知道,首屏时间一定比白屏时间长,因为首屏时间的另一种计算是:首屏时间=白屏时间+首屏开始渲染至渲染结束的时间。
35. 说三种刷新页面的方式?
- 原生js方式:location.reload(),我们只需要在需要刷新的地方加这么一行代码即可做到刷新。
- 借助Vue中的路由跳转方式:如果需要刷新,则写入 this.$router.go(0),这个方法解释一下,就是我们要跳转路由,而需要跳转的页面就是本页面,所以是go(0),这样就可以做到页面的重新加载。
- provide / inject 组合方式。前两种方法都是强制刷新,页面会出现短暂空白,而provide/iniect 是普通刷新,不会使页面出现空白。这个方法,允许一个祖先组件通过provide向其所有子孙后代提供数据,不论组件层次有多深,子组件都可以通过inject来注入,接收这个数据。
具体实现:在项目的app.vue文件中定义一个布尔类型的数据,通过v-if来控制 router-view是否展示,同时定义一个刷新函数,函数内部逻辑主要是将展示设为false,等nextTick执行后再将展示设为true,实现页面的重新加载;写好刷新函数后,在需要刷新的组件中通过inject注入刚刚app.vue中provide提供的依赖,也就是那个刷新函数,然后直接调用这个函数即可实现刷新。
总结刷新函数做的事情就是,想要刷新的时候,我们就调用刷新函数,结合v-if的作用,先将组件是否展示设为false让组件先销毁,再将是否展示设为true让组件创建。除了以上方式,还有this.$forceUpdate()等刷新方式。
36. vue中如何单独更改某个页面的背景颜色?
【需求】全局页面背景色是白色,现需要更改某个页面的背景色为灰色。
【无效】尝试直接改body标签的样式,但是设置后,发现所有页面背景色都变成灰色了。
【原因】vue是一个单页面应用,只有一个index.html,牵一发而动全身。
【正确做法】在这个页面创建前,也就是beforeCreate生命周期函数里把body背景色改成我们想要的颜色,同时在这个页面销毁前,也就是beforeDestroy生命周期钩子中,移除我们刚加的背景色样式。这样跳到其他页面时,刚在那个页面加的body背景色就会移除,继续使用全局的那个背景样式。
具体代码是:
beforeCreate(){
document.querySelector('body').setAttribute('style','background-color:#fff')
},
beforeDestroy() {
document.querySelector('body').removeAttribute('style')
},
除了在需要修改的页面上里改body的背景样式外,我们也可以把这块逻辑封在路由守卫中,当进入需要更改的页面路由时,做刚刚的样式操作。离开这个路由时移除。
37. ref的作用以及使用场景?
- ref 可以用来获取dom元素。如果我们给一个元素绑定ref=“test”,那么我们就可以通过
this.$refs.tes
t获取dom,然后做一些我们需要的操作。 - ref 也可以获取子组件中的方法或者 data等。如果给子组件上绑定一个ref,值依然假设为test,那么在父组件中通过
this.$refs.test
,就可以拿到一个VueComponent对象,这个对象里面有这个子组件的各个属性,打印出来会发现里面有个$el
属性,这就是这个子组件的dom对象。如果子组件的data中有个msg属性,那么在父组件内我们就可以通过this.$refs.test.msg
拿到子组件的这个msg值;再假设子组件有一个getData方法,那么父组件内通过this.$refs.test.getDgta()
也可以调用子组件的getData方法。
38. 静态资源放在哪里?assets和static的区别?
- vue项目的目录结构通常在src目录下有个assets文件夹,和src同级的地方有个static文件夹。
【相同点】两个文件夹下都可以用于存储项目中所需的静态资源,像图片,样式文件等等。
【区别】assets下存放的静态资源文件在项目打包时,也就是执行 npm run build 指令时,会走webpack的打包流程,做压缩代码体积、代码格式化这种操作;放static中存放的资源文件不会走打包流程,而是直接复制进最终的dist目录里。所以如果找们把所有资源都放进static下,由于该文件夹下的资源不会走打包流程,所以在项目打包时会提高一定的效率,但是同时也有一个问题,就是由于不会进行压缩等操作,所以项目打包后的体积会比把资源都放进assets下来得大。
【总结】我们通过npm run build打包项目后,会生成一个dist文件夹,放在assets里面的资源会被webpack打包后放进dist文件夹中,而static里面的资源是直接复制进dist中,由于第三方类库资源一般都已经经过处理了,所以我们可以在static里放一些外部的第三方资源文件,assets放我们自己项目中的图片等资源,让这些资源走打包流程,减少最终包的体积。但是实际开发中情况肯定是多变的,还是要根据实际情况来看把静态资源文件放在哪里更合适。
39. vue中route和router的区别?
this.$router
是VueRouter的一个实例,是一个全局路由对象,它可以用来操作路由,项目中比较常用的就是拿来做路由跳转,比如经常我们需要跳转到另一个页面时,就会写this.$router.push( )
,this.$route
是当前激活的路由对象,通过它我们可以拿到当前路由的一些信息比如path,,query,meta等属性。- 比如我们希望查看某条数据的详情,点击详情时需要跳转路由到详情页面,这里就可以通过
this.$router.push
来做跳转,并且我们可以给即将跳转到的详情路由对象的query对象里传个该条数据的id,为的是希望路由跳转后,通过这个id去获取数据的详情并展示,跳转后怎么拿id呢,我们就可以通过this.$route
先拿到当前路由的所有信息,然后去query对象里拿刚刚在上一页面跳转前放进去的id属性,这样拿到id后再去查详情数据。完整的就是this$route.query.id
。
概括来说就是,route 是用来获取当前路由信息的,也就是读路由信息,而router是用来操作路由的,是写路由的。
40. 怎么判断进入了可视区域?
41. 平时项目中怎么做跳转?
- 第一个方法就是标签,我们可以在标签里面添加to属性来配置需要跳转的路径,浏览器解析的时候会将其解析为类似于a标签的东西,
- 第二种方案就是
this.$router.push()
方法,我们可以在某个函数里面要用路由的这个方法来实现跳转。 - 第三种方法是
this.$router.replace()
,this.$router.push()
跳转到指定url路径的同时也会像history栈中添加一条记录,点击后退就会返回到上一个页面,this.$router.replace()
方法跳转到指定URL 路径点击返回,他是跳转到你看到的上上个页面也就是说目标页面直接替换了,而不是添加一条记录, - 第四种方法
this.$router.go(n)
,我们可以利用这个方法向前或者向后跳转n个页面,比如说n是1,可以跳到下一页,如果是-1,则回退到上一页,0就是当前页面。
43. 项目中如何做到echarts图表自适应?
【需求】浏览器窗口大小变化的时候,echarts图表要随着浏览器窗口变化而变化。
【解决】window.addEventListener方法监听窗口的变化,当窗口变化时,让需要自适应的echarts实例调用echarts官方给的自适应resize方法,可以在mounted钩子函数中写下面的代码,这样就可以实现自适应了。
window.addEventListener('resize', () => {
myEchart.resize();
});
44. 说几种图片懒加载的实现方式?
- 背景及原理
- 在前端项目中,当页面有很多图片的时候,图片加载就需要一定的时间,这样是很耗费服务器性能的,不仅影响渲染速度还会浪费带宽,为了解决这个问题,提高用户体验,所以就出现了懒加载这种方式来减轻服务器的压力,就是优先加载可视区域的内容,其他部分的内容等进入了可视区域再进行加载,从而提高性能。
- 实现思路
- 图片都是根据src属性进行加载的,所以我们可以在图片没有进入可视区域前,先不给src赋值(或者可以先给一个很小的loading图的地址),等到图片进入可视区域再给src赋真正的值。图片的真实地址可以先存储在data-src中。了解了实现思路,那继续最关键的一步,那就是如何计算图片是否进入了可视区域?
- 具体实现方式
<div>
<h6>图片懒加载</h6>
<img data-src="/static/images/login-bg-3.jpg" src="/static/images/login-bg-4.jpg"><br>
<img data-src="/static/images/login-bg-1.jpg" src="/static/images/login-bg-4.jpg"><br>
<img data-src="/static/images/login-bg.jpg" src="/static/images/login-bg-4.jpg"><br>
<img data-src="/static/images/login-bg-3.jpg" src="/static/images/login-bg-4.jpg"><br>
<img data-src="/static/images/login-bg-old.jpg" src="/static/images/login-bg-4.jpg"<br>
<img data-src="/static/images/login-bg-1.jpg" src="/static/images/login-bg-4.jpg"><br>
<img data-src="/static/images/login-bg.jpg" src="/static/images/login-bg-4.jpg"><br>
</div>
第一种:vue-lazyload 插件实现
(1) 安装插件
npm install vue-lazyload --save-dev
(2) 在main.js文件中引入并使用
import VueLazy from ‘vue-lazyload’
Vue.use(VueLazyload)
(3) 修改图片显示方式为懒加载即可。将:src=“xxx” 属性直接改为v-lazy=“xxx”
第二种:IntersectionObserver API 实现。
这个api可以自动"观察"元素是否可见,Chrome 51+ 已经支持。由于可见的本质是,目标元素与视口产生一个交叉区,所以这个 API 又叫做交叉观察器。
created() {
this.intersectionObserver();
},
methods:{
intersectionObserver(){
let images = document.getElementsByTagName('img');
const observer = new IntersectionObserver((imgs) => {
console.log('imgs===', imgs)
// imgs: 目标元素集合
imgs.forEach((img) => {
// img.isIntersecting代表目标元素可见,可见的时候给src赋值
if (img.isIntersecting) {
const item = img.target
item.src = item.dataset.src
observer.unobserve(item);
}
})
})
//定时器和Array.from的目的是让images可遍历
setTimeout(()=>{
Array.from(images).forEach(item => {
observer.observe(item);
})
},300)
}
}
// isIntersecting 为true 代表该目标元素可见,可以加载;target 即对应页面中的真实img。
元素的offsetTop API 实现。
判断进入可视区域的条件有变,通过下图,可以看出,这里进入可视区域的判断条件是某一元素e的 e.offsetTop < document.body.clientHeight + document.body.scrollTop
获取对象相对于版面或由 offsetTop 属性指定的父坐标的计算顶端位置 < 网页可视区域的高度 + 网页被卷去的高度
本文标签: 面试题
版权声明:本文标题:2023前端面试题汇总 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.freenas.com.cn/jishu/1726435537h960143.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论