theme: channing-cyan
- 求关注😭
壹、HTML + CSS
1. 对HTML语义化的理解
- 去掉或者丢失样式的时候能够让页面呈现出清晰的结构;
- 代码结构清晰,方便团队的管理和维护,并且语义化更具可读性;
- 提升用户体验,在没有样式表的情况下,也能呈现比较良好的页面结构效果;
- 有利于SEO优化,提升搜素引擎排名;
- 方便其他设备的解析(如屏幕阅读器、盲人阅读器、移动设备)。
2. H5新增性
- 语义化更好的标签(
header
、nav
、aside
、article
、session
、footer
); - 音频、视频标签(
audio
、video
); - 以
data-
开头的自定义属性; - 本地存储(
localStorage
、sessionStorage
); - 表单控件(
url
、search
、file
、email
、date
、number
、month
、color
、tel
); - 画布(
Canvas
); - 新的技术
webWorker
、webStorage
。
3. 什么是重绘和重排(回流)?
- 当元素的一部分属性发生改变,如外观、背景、颜色等不会引起布局变化,只需要浏览器根据元素的新属性重新绘制,使元素呈现新的外观叫做重绘,重绘不一定需要重排比如颜色的改变,重排必然导致重绘,比如改网页位置。
4. 如何画一个三角形
- 给一个 块级元素 设置 宽高为0 ,
border: 5px solid transparent;
根据项目需求给对应方向的边框设置颜色即可。
5. 如何实现一条0.5px的border
- 给一个块级元素设置相对定位,并设置其伪元素为绝对定位,伪元素的大小是父级的2倍(
200%
),让伪元素缩小一半,再设置伪元素的远点为(0, 0)
。 -
/* 四边框*/ <style> div { position: relative; width: 100px; height: 100px; } div::before { content: ''; position: absolute; top: 0; left: 0; width: 200%; height: 200%; border: 1px solid #333; transform: scale(.5); transform-origin: 0 0; } </style>
6. 介绍一下 CSS盒模型
- 组成:
margin + border + padding + content
- 分类:
- 标准盒模型:
width = content
- 设置的 width 只是盒子内容的宽度
- IE盒模型(怪异盒模型):
width = content + border + padding
- 标准盒模型:
7. 什么是 flex布局
- 定义:可以最大限度的扩展和收缩容器内的元素,以最大限度的填充空间;
- 组成:弹性容器 + 弹性盒子 + 主轴+ 侧轴;
- 主轴对齐方式:
juetify-content(space-around、space-between、space-evenly、center)
; - 侧轴对齐方式:
align-items(center)
; - 改变轴向:
flex-descraption
; - flex属性:属性值数字,表示的意思:将剩余空间均分,flex为几就占几份。
8. 什么是 BFC
- 块级格式化上下文:它是一个独立的渲区域,可以把他当作一个盒子,在这个盒子内部元素按照一定的规则布局,且不会影响其他盒子的布局。既BFC种的元素不受外界的影响。
- 作用:清除外部浮动、防止浮动元素覆盖、组织浮动元素重叠。
- 生成BFC的方式:
float
属性 不为none
;position
为absolute
或fixed
;display
为inline-block
、table-cell
、table-caption
、flex
、inline-flex
;overflow
不为visible
。
- 特点:
- 内部的盒子再垂直方向上从上到下排列,盒子垂直方向的间距由maigin决定;
- 不同BFC种的盒子依然会margin重叠和穿透(比如body就是一个BFC,但他内部依然会出现margin重叠),不同BFC则不会;
- BFC就是页面上的一个隔离的独立容器,容器内部的子元素不会影响到外面的元素;
- 计算BFC的高度时,浮动元素的高度也要计算。
8. position 属性比较,说一下每个属性的区别
relative
:相对定位(相对自身原来的位置,不脱离标准流);absolute
:绝对定位(相对具有 position属性 的祖先元素,脱离标准流);fixed
:固定定位(相对于浏览器可视化窗口定位,脱离标准流);sticky
:粘性定位(相对于浏览器可视化窗口定位,脱离标准流);static
:静态定位(默认状态)。
9. 为什么需要浮动,浮动带来的影响
- 为什么需要浮动?
- 实现多个块级元素一行显示;
- 实现两个盒子一左一右的布局方式。
- 浮动带来的影响?
- 元素浮动之后,脱离标准流,撑不开父级的高度,影响后面元素的布局。
10. 清除浮动的方法
- 给父级设置高度’
- 给父级设置
overflow: hidden / auto;
; - 额外标签法:在父级的最后面添加一个块级元素,添加
clear: both;
属性; - 单伪元素法;
- 双伪元素法。
11. CSS选择器有哪些,优先级
- 选择器:id选择器、类选择器、伪类选择器、属性选择器、标签选择器、伪元素选择器、通配符、后代选择器、交集选择器、并集选择器,子代选择器。
- 优先级:
- !important > 行内样式(1000) > id选择器 (100)> 类选择器 = 伪类选择器=属性选择器 (10)> 标签选择器=伪元素选择器(1) > 通配符、继承(0)
12. CSS隐藏一个元素的方法有哪些?区别?
- display:none;元素隐藏后不占有原来的位置,引起回流
- visibility:hidden;元素继续占有原来的位置,引起重绘
- opacity:0;元素继续占有原来的位置,引起重绘
- overflow:hidden;溢出隐藏
13. 怎么实现一个盒子水平垂直居中
- margin(margin-top + margin-left)
- position(top + left)
- position + margin
- position + transform
- flex布局(justify-content + align-items)
14. href 和 src的区别?alt 和 title的区别
- 共同点:都是对资源的引入
- 区别:
- href:指向网络资源所在的位置,指向的内容会嵌入到文档中当前标签所在的位置
- src:指向外部资源所在的位置,建立和当前元素或当前文档之间的链接
- alt:当资源加载失败的时候显示的信息(替换文本)
- title:当鼠标悬停在标签上的时候的提示信息(提示信息)
15. px、rem、em的区别
- px:绝对单位,根据电脑屏幕的分辨率决定
- rem:相对单位,相对于根标签字号大小,做移动适配
- em:相对单位,相对于当前标签的字号大小,如果当前标签没设置字体大小,则相对于浏览器的默认字体大小(em会继承父级元素的字体大小)
16. C3新增特性
- flex布局
- IE盒模型
- 2D / 3D转换
- 媒体查询
- 动画 (@keyforms,animation)
- 边框图片、圆角边框
- 文字阴影、盒子阴影
- 渐变、 图片位置(background-position)、背景效果
- 颜色模式:RGBA、HLSA
- 新增选择器(
E:nth-child()
)
17. 如何解决 margin塌陷
- 两个同级元素,垂直排列:两个外边距不能同时出现(避免同时出现两个外边距);
- 嵌套关系:
1)给父元素设置border
2)给父元素设置overflow: hidden;
3)给父元素设置padding
4)给父元素设置position: fixed;
5)利用伪元素给父元素前面添加一个空元素.father::before { content: ''; display: table; }
18. 单冒号 和 双冒号 的 区别及作用
- 区别:
1)单冒号(:)伪类
2)双冒号(::)伪元素 - 作用:
1)::before 和 ::after的主要作用是在元素内容前后加上指定内容,伪类是用来向选择器加特殊效果
2)伪类和伪元素的本质区别在于是否抽象创造了新元素
3)伪类只要不是互斥可以叠加使用
4)伪元素在一个选择器中只能出现一次,并且只能出现在开始和末尾
19. 创建带有ID属性的DOM元素有什么副作用?
- 会增加内存负担;
- 会创建同名的全局变量。
20. 如何实现一个左边固定200px宽度,右边自适应的布局?
方案一:flex布局
.parent{ display: flex; } .left { width: 200px; } .right { flex: 1; }
方案二:calc计算
.left { display: inline-block; width: 200px; } .right { display: inline-block; width: calc(100% - 200px); }
方案三:浮动 / 定位
.left { float: left; width: 200px; } .right { padding-left: 200px; }
贰、JS - 基础
1. JS的数据类型有哪些,区别是什么?
- 基本数据类型
- number、string、boolean、null、undefined、symbol、BigInt(大整数)
- Symbol主要作用:创建对象的唯一属性名,因此可以用来防止属性名冲突,保证属性名的独特性
- 引用数据类型
- Object、Array、Function
- 区别:
1)基本数据类型是存储在栈中的简单数据段,占据空间小,属于被频繁使用的数据;
2)引用数据类型是存储在堆内存中,占据空间大,引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址,通过检索栈中的地址,取得地址后在堆中获得实体。
2. undefined 和 null的区别
- 数据类型不同
- 意义不同
- undefined:声明变量未赋值
- null:声明变量赋值了,赋的是一个空值
- 转数字结果不同(Number())
- undefined ==> NaN
- null ==> 0
- 产生场景不同
- undefined:
- 声明变量未赋值
- 数组 / 对象没有某个元素或属性
- 函数没有返回值、没有传递参数并且没有设置默认参数
- null:使用原生JS获取元素的方法,获取DOM树上没有的元素
- undefined:
3. 如何判断JS的数据类型
- typeof
- 可以判断除null之外的基本数据类型 和 function
- instanceof
- 依据:判断构造函数的原型是否出现在实例的原型链上
- 注意:不能判断基本数据类型
3 instanceof Number // false '3' instanceof String // false
- Object.prototype.toString.call() / Object.prototype.toString.apply()(都能判断)
‘[object Number]’ —— 字符串
需要让toString()函数内部的this指向当前检测的变量
4. 创建对象的方式
- 字面量
- Object构造函数(new Object())
- 自定义的构造函数
5. 创建函数的方式
- 函数声明
- 函数表达式
- 构造函数(new Function())
5. JS常用的内置对象
- Math (Math.floor()、Math.ceil()、Math.abs()、Math.round()、Math.random())
- Number(Numbe.isInteger())
- Object(Object.keys()、Object.values()、Object.assgin())
- Array(push、unshift、pop、shift、sort、splice、reverse、forEach、map、filter、join、find、indexOf、includes、slice、concat)
- String(substring、replace、split、slice、includes、indexOf、toLowerCase、toUpperCase)
- Date(Date.getFullYear()、Date.getMonth()、Date.getDate()、Date.getDay()、Date.getHours()、Date.getMinutes()、Date.getSeconds()、Date.getTime())
- Function(Function.call()、Function.apply()、Function.prototype、Function.arguments[])
6. == 和 === 的区别
- ==(等值符):
- 只比较值
- 数据类型相同:去比较两边的值是否相等
- 数据类型不同:会发生类型的自动转换,转换为相同类型后再做比较
- ===(等同符):
- 既要判断值是否够相等,也要判断数据类型是否相同
- 数据类型相同:直接比较值是否相等
- 数据类型不同:直接返回false
7. 如何区分数组和对象?
- Array.isArray()(ES6新增的判断数组方法)
Array.isArray([]) // true Array.isArray({}) // false
- instanceof:判断构造函数的原型是否出现在实例原型链上
[] instanceof Array // true {} instanceof Array // false
- constructor:找到创建当前实例的构造函数
[].constuctor // Array {}.constuctor // object
- Object.prototype.toString.call()(通过apply或call去改变方法的this指向)
Object.prototype.toString.call([]) // '[object Array]' Object.prototype.toString.call({}) // '[object Object]'
8. 多维数组降重
- 多维数组 => 字符串(join) => 一维数组(split)
const arr = [[222, 333, 444], [55, 66, 77]]; const str = arr.join(); const newArr = str.split(',') console.log(newArr); // ['222', '333', '444', '55', '66', '77']
- 使用flat()方法(小括号里写数组的维数 / 一劳永逸Infinity)
- 展开运算符
// 循环遍历原始数组,对元素进行判断,如果是基本数据类型,直接pish,如果是数组,则配合使用展开运算符push const arr = [[222, 333, 444], [55, 66, 77]]; const newArr = []; arr.forEach(item => { Array.isArray(item) ? newArr.push(...item) : newArr.push(item); }); console.log(newArr); // [222, 333, 444, 55, 66, 77]
- concat()
// 声明一个空数组,循环遍历原始数组,如果元素是基本数据类型,直接push,如果元素是数组,则和新数组进行合并 const arr = [[222, 333, 444], [55, 66, 77], 88, 99]; let newArr = []; arr.forEach(item => { Array.isArray(item) ? newArr = newArr.concat(item) : newArr.push(item) }); console.log(newArr); // [222, 333, 444, 55, 66, 77, 88, 99]
- 递归
9. 怎么判断两个对象相等?
- 转换成字符串,然后使用 === 去判断;
- 使用
JSON.stringify()
进行序列化。 - 缺陷:如果两个属性颠倒位置,检测结果不正确;
-
const a = { a: 1, b: 2 } const b = { b: 2, a: 1 } console.log(JSON.stringify(a) === JSON.stringify(b)) // false
- 使用
- 判断对象的所有属性和属性值相等;
- 使用 Object.keys() 和 Object.values() 获取两个对象的所有属性和属性值,进行以一一比对
- 缺陷:只能判断简单数据,如果某个属性值是个引用数据类型,就不准确了;
- 在第二部的基础上,增加递归算法;
const obj1 = { a: 1, c: 3, b: 2 } const obj2 = { a: 1, b: 2, c: 3 } function isEqual(obj1, obj2) { // TODO: 通过 Object.keys() 将对象的键名转成数组 const arr1 = Object.keys(obj1) const arr2 = Object.keys(obj2) // TODO: 比较两个数组的长度,若长度不相等,也就没必要进行比较了 if (arr1.length != arr2.length) return false // TODO: 变量对象,看对象的值是否相等 for (const k in obj1) { if (typeof obj1[k] == 'object' || typeof obj2[k] == 'object') { if (!isEqual(obj1[k], obj2[k])) return false } else if (obj1[k] !== obj2[k]) return false } return true } console.log(isEqual(obj1, obj2))
10. 列举 强制转换类型 和 隐式转换类型
- 强制转换类型:
- 转数字:(第二个入参表示进制数(2~32))
- Number()(字符串必须是全数字组成,除null(0)和空字符(0))
# 特殊: Number(null) // 0 Number('') // 0 Number() // 0
- parseInt()(从左到右,只转数字)
- parseFloat()(从左到右,只转数字)
- Number()(字符串必须是全数字组成,除null(0)和空字符(0))
- 转字符串:String()、toString()
- 转布尔:Boolean()
- 转数字:(第二个入参表示进制数(2~32))
- 隐式转换类型:+、-、*、/、%、==、>=、<=
11. JS中获取当前日期的月份
const date = new Date();
console.log(`当前年份 - ${date.getFullYear()}`);
// 月份是从0开始的,当前月份 = 得到的月份 + 1
console.log(`当前月份 - ${date.getMonth() + 1}`);
console.log(`当前日期 - ${date.getDate()}`);
console.log(`当前周几 - ${date.getDay()}`);
console.log(`当前小时 - ${date.getHours()}`);
console.log(`当前分钟 - ${date.getMinutes()}`);
console.log(`当前秒 - ${date.getSeconds()}`);
12. 什么是类数组(伪数组),怎么转化为真数组
- 伪数组:具有索引、长度,不能使用数组方法
- 转为真数组:Array.from(伪数组)、展开运算符、使用for循环遍历为数组,声明一个新数组,将每一项push进一个新数组
const obj = { 0: 1, 1: 2, 2: 3, length: 3 }; console.log(Array.from(obj)); // [1, 2, 3]
13. 谈谈对变量的理解
- 变量就是一个用来存储数据的容器
- 本质:内存里的一块空间,用来存储数据
- 初始化:声明变量并进行赋值操作
- 命名规则:变量名只能是数字、大小写字母、美元符、下划线组成,开头不能是数字
- 声明变量:使用 let 关键字
- 使用:必须先声明后使用
14. let、const、var的区别
- let / const:
- ES6新增的
- 不存变量提升
- 有块作用域
- 必须先声明后使用
- 不能声明重复变量
- let声明变量、const声明常量(必须进行初始化)
- var:
- 存在变量提升
- 没有块作用域
- 既可以声明变量也可以声明常量
- 声明过的变量可以重复声明
- 可以先使用后声明
15. for-in 和 for-of 的区别
- for-in:
- 适用:对象、数组、伪数组、字符串
- 遍历当前对象可枚举属性及其原型链上的属性
- 遍历的是属性或索引
- 得到的索引是字符型
- for-of:
- 适用:数组、伪数组、字符串
- 遍历的是(可迭代的数据)数组 / 伪数组的元素、字符串的值
- 得到的索引是数字型
16. 具名函数 和 匿名函数 区别
- 具名函数:
- 可以先使用后声明
- 不能作为事件处理函数
- 可以作用于构造函数
- 匿名函数:
- 必须先声明后使用
- 可以作为事件处理函数
- 使用方式:函数表达式 + 立即执行函数
- 不能作为构造函数使用
17. 手写冒泡排序
使用双重for循环
外层for循环控制排列好的个数(个数 - 1)
内存for循环控制排序好一个数字需要比较的次数
let arr = [2, 46, 32, 86, 45, 98]
// [(2), 46, 32, 86, 45, 98] 0 5
// [2, (32), 86, 45, 46, 98] 1 4
// [2, 32, (45), 46, 86, 98] 2 3
// [2, 32, 45, (46), 86, 98] 3 2
// [2, 32, 45, 46, (86), 98] 4 1
for (let i = 0; i < arr.length - 1; i++) {
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
console.log(arr);
18. 数组和伪数组的区别?
- 共同点:都可以通过索引取值、都有length属性
- 区别:原型不同
1)数组原型:Array.prototype
2)伪数组原型:不一定,是一种集合,不具备数组的方法
叁、JS - WebAPI
1. 什么是DOM?
- Document Object Model — 文档对象模型
- DOM是HTML或XML文档的编程接口,它提供了对文档的结构化的表示,并定义了一方式可以在程序中对网页结构进行访问,从而改变网页的结构、样式和内容。DOM将文档解析为一个由节点和对象组成的结构集合,简单来说,它会将页面和程序连接起来,进而改变网页结构样式行为。
- 顶级对象:document(window.document)
2. DOM结构操作怎样添加、移除、替换、复制、创建和查找节点?
- 创建节点:
- document.createElement()
- 添加、移除、替换、插入节点:
- 添加:appendChild()
- 移除:removeChild()
- 替换:replaceChild()
- 插入:insertBefore()
- 查找结点:
- 父节点:parentNode()
- 亲子节点:children(伪数组)
- 上一个兄弟节点(属性):previousElementSibling
- 下一个兄弟节点(属性):nextElementSibling
3. JS中的定时器有哪些?区别和用法是什么?
- setInterval():间歇函数, 每隔n毫秒就去执行回调函数
- setTimeout():延迟函数,延迟n毫秒后去执行回调函数,只执行一次
- 区别:
- setInterval()方法会不停的调用函数,直到clearInterval()被调用或窗口关闭
- serTimeout()只执行一次
4. 事件监听三要素
- 事件源
- 事件类型
- 事件处理函数
5. 事件流、事件冒泡、事件委托
- 事件流:事件完整执行过程中的流动路径,主要有三个阶段:
- 事件捕获阶段:从祖先元素到目标元素(从大到小,从外到内)
- 事件目标阶段:
- 事件冒泡阶段:从目标元素到祖先元素(从小到大,从内到外)
- 事件冒泡:
- 在一个对象上触发某类事件,这个事件会向该对象的父级对象传播,从里到外,直至它被处理(父级对象所有同类事件都被激活),或者它到达了对象层次的最顶层(document对象)
- 事件委托:将事件绑定在目标元素的祖先元素身上,避免了给多个子元素绑定事件,主要原理是事件冒泡机制
- 优点:
- 减少事件注册,节省内存
- 简化了DOM节点更新时,相应事件的更新
- 可以为未来元素预备事件
- 缺点:
- 事件委托基于事件冒泡,对于不冒泡的事件不支持
- 层级过多,冒泡过程中,可能会被某层阻止掉
- 把所有事件都用代理就可能会出现事件误判
- 优点:
6. JS动画 和 C3动画区别
- JS动画:
- 优点:
- JS动画控制能力强,可以在动画播放过程中对动画进行控制:开始、暂停、回放、停止、取消等
- 动画效果比C3动画更丰富(曲线运动、视差滚动)
- C3动画有兼容性,而JS动画大多时候没有兼容性问题
- 缺点:
- 代码的复杂程度高于C3动画
- JS在浏览器主线程中运行,而主线程中还有其他需要运行的JS脚本、样式计算、布局、绘制任务等,对其他干扰导致线程可能出现阻塞,从而造成丢帧的情况
- 优点:
- C3动画:
- 优点:
- 浏览器可对动画进行优化
- 代码相对简单,性能调优方向固定
- 相对帧速表现不好的低版本浏览器,C3可以自动降级,而JS需要撰写额外代码
- 缺点:
- 运行过程控制较弱,无法附加事件绑定回调函数
- 代码冗长。实现复杂的动画,CSS代码就会变得非常笨重
- 优点:
- C3和JS动画差异:
- 代码复杂程度,JS动画代码相对复杂一些
- 动画运行时,对动画的控制程度上,JS能够让动画暂停、取消、终止,CSS动画不能添加事件
- 动画性能看,JS动画多了一个JS解析的过程,解析功能不如C3动画好
- 总结:
- 简单的状态切换,不需要中间过程控制,C3动画是优选方案
- 复杂的动画,应该使用JS动画去实现
7. DOM 和 BOM 区别
- DOM:
1)Document Object Model — 文档对象模型
2)DOM是W3C的标准
3)DOM的顶级对象的document(实际上是window.document) - BOM:
1)Browser Object Model — 浏览器对象模型
2)BOM没有相关标准
3)BOM的顶级对象是window
8. 什么是window对象?什么是document对象?
- window对象:
1)Window对象表示浏览器中打开的窗口
2)所有的全局函数和对象都属于Window对象的属性和方法
3)它是一个顶层对象,而不是另一个对象的属性,既浏览器的窗口 - document对象:
1)该对象是window对象的一个属性,是显示于窗口的一个文档 - 区别:
1)window指窗体。document指文档。document是window的一个子对象
2)用户不能改变document.location(因为这是当前显示文档的位置)。但是,可以改变window.location(用其他文档取代当前文档)。window.location本身是一个对象,而document.location不是对象
9. 描述渲染的过程,DOM树和渲染树的区别?
- 浏览器的渲染过程:
1)解析HTML构建DOM树,并行请求css/image/js
2)CSS文件下载完成,开始构建CSS树
3)CSS树构建结束后,和DOM树一起生成渲染树 - DOM树和渲染树的区别:
1)DOM树与HTML标签一一对应,包括head和隐藏元素
2)渲染树不包括head和隐藏元素,大段文本的每一个行都是独立节点,每一个节点都有对应的css属性
10.如何最小化 重绘 和 回流
- 重绘:当前元素的一部分属性改变,但不会引起布局的变化,只需要浏览器根据元素的新属性重新绘制,是元素呈现新的外观叫做重绘;
- 回流(重排):当渲染树中的一部分或者全部因为大小边距等问题发生改变而需要DOM树重新计算的过程(元素的一部分属性改变,并且会引起布局的变化,导致DOM树重新计算的过程);
- 重绘不一定引起回流,但回流一定引起重绘;
- 最小化方法:
1)需要对元素进行复杂操作时,可以先隐藏,操作完成后再显示
2)需要创建多个DOM节点时,等创建完毕后,一次性加入document
3)尽量使用css属性简写,如:用border代替border-width、border-style、border-color
4)批量修改元素样式(可以使用新增类名和移除类名的方法)
11. 正则表达式 元字符 和 修饰符
- 元字符:是一些具有特殊含义的字符:
- 边界符:表示开头和结尾(^、$)
- 量词(*、+、?、{n}、{n,}、{n,m})
- 字符类
- 匹配字符集合([])
- 连字符(-)
- 取反符号(^)
- 预定义类
| 预定义类 | 说明 |
| -------- | ------------------------------------------------------------ |
| \d | 匹配 0-9 之间的任意数字,相当于 [0-9] |
| \D | 匹配所有0-9以外的字符,相当于[ ^0-9] |
| \w | 匹配任意的字母、数字和下划线,相当于[a-zA-Z0-9_] |
| \W | 除所有字母、数字和下划线以外的字符,相当于[ ^a-zA-z0-9_] |
| \s | 匹配空格(包括换行符、制表符、空格符等),相等于[\t\r\n\v\f] |
| \S | 匹配非空格字符,相当于[ ^\t\r\n\v\f] |
- 修饰符:i(不区分大小写)、g(全局匹配)、m(多行匹配)
12. 合并数组的方法
- concat()方法
- arr1.concat(arr2)
- push() + 展开运算符
- arr1.push(…arr2)
- 展开运算符
- newArr = […arr1, …arr2]
13. 结束(终止)forEach
- 错误用法:break(会报错)、return(只是终止本次循环)
- forEach专门用来循环数组,可以直接取到元素,同时也可以取到index的值,存在局限性,不能continue跳过或者break终止循环,没有返回值,不能return
- 终止forEach循环:运用抛出异常(try-catch)可以终止forEach循环
14. 哪些数组方法中间return不会影响遍历次数
- forEach、map、filter、reduce
15. 关于JS中弹框的问题
confirm()
:确认框alert()
:警示框prompt()
:对话框open()
:打开新的窗口或寻找已命名的窗口
肆、JS - 高级
1. 解释一下作用域链
- 作用域:规定了变量能够被访问的范围,离开这个范围,变量便不能被访问(变量的作用范围)
- 嵌套关系的作用域串联起来形成了作用域链
- 本质:底层变量的访问机制
1)在函数执行时,会优先在当前函数作用域中查找变量
2)如果当前作用域查找不到,则会依次逐级向上查找,直到全局作用域
2. typeof 和 instanceof 区别
- typeof只能判断 除null之外的基本数据类型 、 function 以及不存在的变量,判断null和别的引用数据类型得到的都是object;
- instanceof能准确判断引用数据类型,不能判断基本数据类型。
- 机制:判断构造函数的原型是否出现在实例的原型链上。
3. 什么是闭包?
- 如果一个内部函数访问了外部函数的变量,那么这个外部函数就会形成闭包。
- 简单来说:内存函数 + 外层函数的变量 = 闭包
- 作用:
1)实现数据的私有化
2)延长变量的作用范围 - 特点:会导致内存泄漏
4. 什么是内存泄漏?哪些操作会造成内存泄漏?
- 内存泄漏:不再使用的内存,没有得到及时的释放,结果导致一直占据该内存单元
- 造成内存泄漏的操作:
1)setTimeout的第一个参数使用字符串而非函数
2)闭包、控制台日志、循环(在两个对象彼此引用且彼此保留时,就会产生一个循环)
5. 谈谈你对JS垃圾回收机制(GC)的理解?
- JS中内存的分配和回收都是自动完成的,内存不使用的时候会被垃圾回收器自动回收
- 标记清除发:
- 当变量进入执行环境的时候(声明一个变量),GC将其标记为进入环境,当变量离开环境的时候(函数执行结束),将其标记为离开环境
- 垃圾回收器会在运行的时候给存储在内存中的所有变量加上标记,然后去掉环境中的变量以及被环境中变量引用的变量(闭包),在这些完成之后仍存在标记的就是要删除的变量
- 引用计数法:
- 引用计数法的策略是跟踪记录每个值被使用的次数,当声明了一个变量,并将一个引用类型赋值给该变量的时候这个值的引用次数就加1,如果该变量的值变成了另外一个,则这个值的引用次数减1,当这个值的引用次数变为0的时候,说明没有变量在使用,因此可以将其占用的内存空间回收,这样垃圾回收器会在运行的时候清理掉引用次数为0的值占用的空间
6. 对预解析的理解
- 变量的声明提升:
- 在代码执行之前,检测在当前作用域下所有用var声明的变量,并将这些变量的声明提升到当前作用域的最前面
- 注意:只提升声明,不提升赋值
- 函数的声明提升:
- 代码执行之前,会将所有函数声明提升到当前作用域的最前面
- 注意:
- 只提升声明,不提升调用
- 函数表达式不存在函数提升(必须先声明后调用)
- 函数提升优先级高于变量提升
7. 箭头函数 和 普通函数的区别?
- function关键字:箭头函数没有,普通函数有
- 动态参数(arguments):箭头函数没有,普通函数有
- this:箭头函数没有,普通函数有
- 一个参数并且没设默认值:箭头函数可以省略小括号,普通函数必须写
- 函数体只有一句代码并作为返回值:箭头函数可以省略大括号和return,普通函数必须写
- 箭头函数不能是构造函数
- 箭头函数替代原本需要匿名函数的地方(函数表达式的简写方式)
8. new操作符干了什么?
- 隐式的创建一个空对象;
- 将构造函数this指向空对象;
- 执行构造函数函数体,修改this,添加新属性或 方法;
- 返回新对象。
9. ES6新增特性
- let、const;
- 展开运算符(…);
- 解构赋值;
- 箭头函数;
- 函数参数默认值;
- 数组方法(splice、sort、forEach、map、filter、reduce、some、every、join、Array.form()、find、concat、reverse……);
- 字符串方法(substring、startsWith、endsWith、split、includes、replace……);
- 模板字符串(``);
- 异步机制:Promise;
- Map和Set;
- 模块导入和导出(import关键字);
10. Object静态方法有哪些?
- Object.keys(对象) — 获取该对象的所有属性名
- 返回值:数组
- Object.values(对象) — 获取该对象的所有属性值
- 返回值:数组
11. 数组常用方法有哪些?
-
改变原始数组:
- arr.push(参数) — 向数组末尾追加一些元素
- 返回值:追加元素之后数组的长度
- arr.unshift(参数 ) — 向数组起始位置插入一些元素
- 返回值:插入元素之后数组的长度
- arr.pop() — 删除数组最后一个元素
- 返回值:被删除的元素
- arr.shift() — 删除数组第一个元素
- 返回值:被删除的元素
- arr.splice(起始元素索引,删除几个,新增/替换的元素) — 删除/替换/添加元素
- 返回值:新数组
- arr.sort() — 数组排序
- 语法:
- 降序:arr.sort((a, b) => a - b)
- 升序:arr.sort((a, b) => b - a)
- 返回值:新数组(排序好的数组)
- 语法:
- arr.reverse() — 反转数组
- 返回值:新数组
- arr.push(参数) — 向数组末尾追加一些元素
-
不改变原始数组:
- arr.forEach(function(item, index, arr){ 函数体 }) — 循环遍历数组
- 没有返回值
- arr.map(function(item,index,arr){ 函数体 }) — 迭代数组(映射数组)
- 返回值:处理之后的新数组
- arr.filter(function(item,index,arr){ 函数体 }) — 筛选数组
- 返回值:新数组(将满足条件(条件为true)的元素筛选出来放心一个新数组并返回)
- arr.reduce(function(prev(累计值),item,index,arr){ 函数体 },0(起始值))
- 返回值:返回函数累计的处理结果
- arr.join(“连接符号”)
- arr.some(function(item,index,arr){ 函数体 }) — 判断数组中是否有满足条件的元素
- 返回值:布尔值(true:只要有一个满足就是true,false:都不满足)
- arr.every(fucntion(item,index,arr){ 函数体 }) ---- 判断数组中的元素是否都满足条件
- 返回值:布尔值(true:都满足,false:只要有一个不满足就是false)
- arr.concat(多个数组) — 合并数组
- 返回值:新数组(合并之后的数组)
- arr.slice(开始索引[ ,结束索引 ]) — 提取数组元素
- 返回值:新数组(存放的是提取出来的元素)
- arr.find(function(item,index,arr){ 函数体 }) — 返回数组中满足条件的第一个元素
- 返回值:有满足条件的元素就返回该元素,否则就是undefined
- arr.indexOf(元素[, 开始索引 ] ) — 找到指定元素的索引
- 返回值:有该元素,就是第一个满足条件元素的索引;没有该元素就是 -1
- arr.includes(元素) — 判断数组中是否有某个元素
- 返回值:布尔值(true:有这个元素;false:没有该元素)
- arr.flat(数组维数 / Infinity) — 数组扁平化
- 返回值:降维之后的数组
- arr.forEach(function(item, index, arr){ 函数体 }) — 循环遍历数组
12. 字符串常用方法有哪些?
- str.substring(开始索引 [ ,结束索引 ] ) — 截取字符串
- 返回值:新字符产(指定部分的字符产)
- str.split(”分隔符“) — 将字符串拆分成数组
- 返回值:数组
- str.replace(旧字符,新字符) — 替换字符串中指定的字符
- 返回值:替换之后的字符串
- str.startsWith(目标字符 [,检测位置索引号] ) — 检测是否以某段字符开头
- 返回值:布尔值
- str.endsWith(目标字符 [,检测位置索引号 ]) — 检测是否以某段字符结尾
- 返回值:布尔值
- str.includes(目标字符 [,检测位置索引号 ]) — 判断一个字符串是否包含在另一个字符串中
- 返回值:布尔值
- str.toUpperCase() — 将字符串全部转为大写字母
- str.toLowerCase() — 将字符串全部转为小写字母
- str.indexOf(目标字符) — 得到目标字符在字符串中的索引
- 返回值:有该字符:得到该字符的索引值;没有该字符:-1
13. 面向过程编程 和 面向对象编程
- 面向过程编程:
- 定义:分析出解决问题所需要的步骤,然后用函数将这些步骤一步一步实现,使用的时候再依次调用就可以
- 优点:性能比面向对象高,适合跟硬件联系很紧密的东西
- 缺点:没有面向对象易维护、易复用、易扩展
- 面向对象编程(oop):
- 定义:把事务分解成一个个的对象,然后由对象之间分工合作
- 特性:封装性、继承性、多态性
- 优点:易维护、易复用、易扩展,基于面向对象封装、继承、多态的特性,可以设计出低耦合的系统,十系统后更加灵活,更加易复用
- 缺点:性能低
14. 说说你对原型(prototype)的理解
- JS规定,每一个构造函数都有一个prototype属性,指向另一个对象,我们称为原型对象。
- 原型的主要作用就是为了实现继承与扩展对象。
- 这个对象可以挂载函数,对象实例化不会多次创建空间,更加节约内存,解决了构造函数浪费内存的问题。
15. 介绍一下原型链
- 当访问一个对象的某个属性时,会优先在自身查找,如果没有就去它的原型身上查找(实例的 __ proto __ 指向的prototype),如果还没有就去原型的原型身上查找(构造函数的prototype的 __ proto __ 指向的原型),一直到null(object.prototype.__ proto __)为止(若object.prototype上面没有得到的就是undefined),将这种链式的查找机制称为原型链。
16. 继承的实现
- 原型链继承
- 优点:可以访问父类的属性和方法以及原型上的属性和方法
- 缺点:继承如果是引用数据类型,其中一个子类进行修改,全部都会受到影响,造成实例共享
- 构造函数继承
- 优点:可以保证每个子类维护自己的属性
- 缺点:无法访问原型链上的属性和方法
- 组合继承(将两个结合)
- 优点:既可以访问原型上的属性和方法,又可以每个子类维护自己的属性
- 缺点:每次创建一个子类实例,父类都会被执行一次
17. 介绍this各种情况
- 普通函数,谁调用,this指向谁
- 当调用者不明确的时候,this指向window
- 以方法(普通函数)的形式调用,this指向对象
- 构造函数和原型中的this,都指向实例对象
- 箭头函数没有 this,沿用的是它创建环境的this
18. call、 apply、bind区别
- 共同点:
- 都能改变this指向
- 第一个参数都是this要指向的对象
- 都可以利用后续参数给函数传参
- 不同点:
- call和apply都是调用函数,只是参数不同,call接收的是参数列表,apply接收的是参数数组,返回值都是函数的返回值
- bind返回一个新函数(已经改变好this指向的函数),参数和call相同,接收的是参数列表
19. EventLoop
- JS在执行代码的时候,将任务分为同步任务和异步任务,将同步任务放在主线程执行栈中执行,异步任务在异步队列中排队等候;事件循环是一种轮询机制,先执行主线程里面的同步任务,待所有的同步任务执行完毕,系统就会依次序读取任务队列中的异步任务;异步任务分为宏任务和微任务,宏任务在宏任务队列中,微任务在微任务队列中,宏任务和微任务是交替执行的,在执行宏任务之前,先检查微任务队列中是否有微任务执行,如果有,就先执行完所有的微任务,再去执行宏任务,每执行完一个宏任务都会去检查微任务队列中是否有微任务要执行,如果有,就执行完所有的微任务再去执行下一个宏任务,如果没有,就继续执行宏任务,将这种循环不断的机制称为事件循环。
20. 常见的宏任务和微任务
- 宏任务:
- 异步Ajax请求
- 定时器(setTimeout() 和 setInterval())
- 文件操作
- 微任务:
- Promise.then()、Promise.catch()、Promise.finally()
- process.nextTick()
21. 防抖 和 节流
- 防抖:是在事件发生后一段时间再执行,如果这段时间内继续触发新的事件,那么取消之前的使事件,只执行最新的事件。
- 应用场景:
- 搜索框(用户在不断收入关键字时,用防抖来节约请求资源)
- 页面尺寸变化(window触发resize的时候,不断调正浏览器窗口的大小会不断出发这个事件,用防抖来让其只触发一次)
- 实现:延迟函数 + clearTimeout
- 代码展示:
// 防抖:sh const div = document.querySelector('div'); let i = 0; div.addEventListener('mousemove', debounce(mouseMove, 200)); function mouseMove() { div.innerHTML = ++i; } function debounce(fn, t) { let timerId = null; return function () { if (timerId) clearTimeout(timerId); timerId = setTimeout(mouseMove, t); } }
- 应用场景:
- 节流:一段时间内只执行一次事件,执行结束后才能继续执行新的事件。(限制任务执行频率的一种手段)
- 应用场景:轮播图、页面尺寸的变化、滚动条、点击按钮……
- 实现:开始时间 - 结束时间 ? 指定时间
- 代码展示:
let i = 0; const div = document.querySelector('div'); div.addEventListener('mousemove', throttle(mouseMove, 200)); function mouseMove() { div.innerHTML = ++i; } function throttle(fn, t) { let timeStart = 0; return function () { const timeEnd = new Date().getTime(); if (timeEnd - timeStart > t) { mouseMove(); timeStart = timeEnd; } } }
22. 浅拷贝 和 深拷贝
- 浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝,如果属性值是基本数据类型,拷贝的就是基本类型的值,如果属性是引用数据类型,拷贝的就是内存中的地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象
- 浅拷贝和直接赋值的区别:
- 直接赋值:当我们把一个对象赋值给一个新的变量时,赋的其实是该对象在栈中的地址,而不是堆中的数据。也就是说两个对象指向同一个存储空间,无论哪个对象发生变化,其实都是改变的存储空间的内容,因此两个对象是联动的
- 浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响
- 深拷贝是将一个对象从内存中完整的拷贝一份出来,从内存中开辟一个新的区域存放新对象,且修改新对象不会影响旧对象
- 简单来说:
- 浅拷贝:拷贝的是栈里面的地址
- 实现方式(常用)
- 展开运算符
- Object.assign()
- 循环原始数据,从新声明一个变量,将元素重新加入到新变量中
- 深拷贝:拷贝的是堆里面的数据
- 实现方式:
递归
- 遇到数组:调用递归函数解决数组
- 遇到对象:调用递归函数解决对象
- 基本数据类型或函数:直接赋值
- 代码展示:
const obj = { name: '邵秋华', age: 23, city: ['海口', '武汉', '上海', '北京'], xiXi: { a: 1, b: 2, c: { d: 3 } }, sayHi: () => { }, girl: undefined } // const newObj = JSON.parse(JSON.stringify(obj)) function cloneDeep(newObj, oldObj) { Object.keys(oldObj).forEach(item => { if (Array.isArray(oldObj[item])) { newObj[item] = [] cloneDeep(newObj[item], oldObj[item]) } else if (oldObj[item] instanceof Object) { newObj[item] = {} cloneDeep(newObj[item], oldObj[item]) } else { newObj[item] = oldObj[item] } }) } const newObj = {} cloneDeep(newObj, obj) newObj.xiXi.c.d = 18 console.log(obj, newObj);
_.cloneDeep()
JSON(序列化与反序列化)
- 原理:先转成字符串(基本数据类型),再转为引用数据类型(在堆里面重新开辟一块空间)
JSON.stringify()
JSON.parse()
- 缺点:不能识别 函数 和 undefined(会丢失)
23. 数组去重
- forEach + includes + push
- 文字描述:声明一个空数组,使用forEach循环遍历数组,判断新数组里面有没有我要push的这个元素,如果有,就不追加,如果没有就追加
- 代码展示:
const arr = [11, 22, 23, 11, 67, 23, 5, 6, 5, 90, 78, 67]; const newArr = []; arr.forEach(item => { if (!newArr.includes(item)) newArr.push(item); }); console.log(newArr);
- forEach + indexOf + push
- 文字描述:声明一个空数组,遍历数组,判断新数组里面有没有要追加的元素,如果有就不追加,否则就追加
- 代码展示:
const arr = [11, 22, 23, 11, 67, 23, 5, 6, 5, 90, 78, 67]; const newArr = []; arr.forEach(item => { if (newArr.indexOf(item) === -1) newArr.push(item); }); console.log(newArr);
- Array.from + Set
- 文字描述:使用 Set()方法,Set中的元素只允许出现一次(单纯使用Set方法得到的是一个伪数组)
- 代码实现:
const arr = [11, 22, 23, 11, 67, 23, 5, 6, 5, 90, 78, 67]; console.log(Array.from(new Set(arr)));
- sort + forEach+ splice
- 文字描述:使用sort()方法先对数组进行排序,然后判断当前元素和下一个元素是否相等,如果相等就删除当前元素或下一个元素
- 代码展示:
const arr = [11, 22, 23, 11, 67, 23, 5, 6, 5, 90, 78, 67]; arr.sort((a, b) => a - b); arr.forEach((item, index) => { if (item === arr[index + 1]) arr.splice(index, 1); }); console.log(arr);
- forEach + find
- 文字描述:声明一个空数组,循环遍历数组,在新数组里面能不能找到当前要追加的元素,如果能找到就不追加,找不到就追加
- 代码展示:
const arr = [11, 22, 23, 11, 67, 23, 5, 6, 5, 90, 78, 67]; const newArr = []; arr.forEach(item => { if (!(newArr.find(item1 => item === item1))) newArr.push(item); }); console.log(newArr)
- 双层for循环
- 文字描述:
- 代码展示:
const arr = [11, 22, 23, 11, 67, 23, 5, 6, 5, 90, 78, 67]; const newArr = []; for (let i = 0; i < arr.length; i++) { newArr.push(arr[i]); for (let j = 0; j < newArr.length; j++) { if (newArr.includes(arr[i])) newArr.splice(i, 1); } } console.log(newArr);
- filter + indexOf
- 文字描述:筛选出满足条件的元素,条件:用indexOf方法获取原数组的索引值,如果原数组的索引值和当前元素的索引值相等,就返回到一个新的数组
- 代码展示:
const arr = [11, 22, 23, 11, 67, 23, 5, 6, 5, 90, 78, 67]; const newArr = arr.filter((item, index) => { return arr.indexOf(item) === index; }); console.log(newArr);
24. 数组的随机排序
// 声明两个空数组
// 第一个空数组:存放从0到arr.length - 1的随机数
// 第二个空数组:用来接收按照第一个空数组里面的元素取原数组里面的元素
const arr = [1, 2, 3, 4, 5];
const a = [];
const newArr = [];
for (let i = 0; i >= 0; i++) {
let b = Math.floor(Math.random() * arr.length);
if (!a.includes(b)) a.push(b);
if (a.length === arr.length) break;
}
a.forEach(index => newArr.push(arr[index]));
console.log(newArr);
arr.sort(() => Math.random() - 0.5);
console.log(arr);
25. 构造函数和普通函数的区别
-
this指向不同
1)构造函数:this指向实例
2)普通函数:this,谁调用指向谁
-
调用方式不同
1)构造函数:new 函数名()
2)普通函数:函数名()
-
函数名不同
1)构造函数:大驼峰命名法
2)普通函数:小驼峰命名法
-
构造函数:
1)构造函数可以new实例对象
2)可以实现继承
3)构造函数不可以是箭头函数
26. JS的作用域
- 作用域 = 全局作用域 + 局部作用域 = 块作用域 + 函数作用域
- 规定了变量能够被访问的范围,离开这个范围,变量便不能被访问
27. 循环遍历对象
- for-in
- 遍历指定对象所有可枚举属性及其原型链上的属性
- Object.keys()
- 可以遍历当前对象上的所有可枚举属性,但是返回值是个数组
Object.defineProperty(obj, 'name', { // 是否可枚举,只有可枚举的数据才能被遍历出来,不可枚举的属性无法被遍历 enumerable: true / false; })
28. cookies、sessionStorage、localStorage的区别
cookie
:是网站为了标识用户身份而存储在本地终端上的数据(通常是经过加密)
- 如果不给cookie设置过期时间,则表示这个Cookie生命周期为浏览器会话期间,只要关闭浏览器窗口,Cookie就消失了
cookie
数据始终在同源http请求中携带(也就是说cookie在浏览器和服务器之间来回传递)而sessionStorage
和localStorage
不会主动把报数据发送给服务器- 存储大小限制不同:
cookie
:不能超过4KB
sessionStorage + localStorage
:虽然也有存储大小限制,但相比cookie要打得到,可以达到5M
甚至更大- 数据的有效期不同:
cookie
:只在设置的过期时效之前一直有效,及时关闭页面或浏览器sessionStorage
:尽在当前页面没有关闭之前有效localStorage
:始终有效,窗口或浏览器关闭也是有效的,除非手动清除- 作用域不同:
cookie
:在所有同源窗口中都是共享的sessionStorage
:只在当前窗口中共享localStorage
:在所有同源窗口中是共享的- cookie并把真都能通过js获取,如果设置了httponly,是获取不到的
29. 多窗口之间 sessionStorage 可以共享吗?
多窗口之间
sessionStorage
不可以共享状态,但是在某些特定场景下新开的页面会复制之前页面的sessionStorage
有两种方式新打开的页面会复制之前的
sessionStorage
window.open("同源页面")
a标签
30. [1, 2, 3].map(parseInt)
[1, 2, 3].map(parseInt)
等价于
[1, 2, 3].map((item, index) => parseInt(item, index))
结果:
[1, NAN, NAN]
如果 基数 超出 2~36 的范围,会返回NaN
1 - 0
2 - 1
3 - 2 2虽然没有超出基数的范围,但二进制里面没有3
31.JS常用的六种设计模式
- 单例模式
- 工厂模式
- 适配器模式
- 装饰器模式
- 策略模式
- 观察者模式
- 发布-订阅模式
伍、Ajax
1. 什么是Ajax?原理是什么
- 定义:在网页中利用XMLHttpRequest对象和服务器进行数据交互的方式就是Ajax
- 原理:通过XMLHttpRequest对象向服务器发异步请求,从服务器获得数据,然后利用JavaScript来操作DOM而更新页面
2. 常见的HTTP状态码以及代表的意义
- 200 => 请求成功
- 201 => 成功请求并创建了新的资源
- 301 => 永久移动(请求的资源已被永久的移动到新的URL,返回信息会包括新的URL,浏览器会自动定向到新的URL,今后任何新的请求都应使用新的URL代替)
- 302 => 暂时移动(资源只是暂时移动,客户端继续使用原来的URL)
- 304 => 未修改(所请求的资源未修改,服务器返回此状态码时,不会返回任何资源)
- 400 => 语义有误、请求参数有误
- 401 => 当前请求需要用户验证
- 403 => 没有权限禁止访问
- 404 => 请求地址有误(服务器无法根据客户端提供的url找到资源)
- 408 => 请求超时
- 500 => 服务器内部错误
- 501 => 服务器不支持某种请求方式
- 503 => 超载、系统维护
3. 同源策略
- 同源:协议、域名、端口号一致就是同源
- 同源策略:浏览器提供的一种安全机制,限制了从同一个源加载的文件或脚本如何与来自另外一个源的资源进行交互,这是一个用于隔离潜在恶意文件的重要安全机制
4. 跨域、如何解决跨域问题
- 跨域:协议、域名、端口号不一致就是跨域,浏览器不能执行其他网站的脚本,它是由浏览器的同源策略造成的,是javascript施加的安全限制,防止他人恶意攻击网站
- 为什么会出现跨域?
当下最流行的就是前后端分离的项目,就是前端项目和后端接口并不在同一域名之下,在前端项目访问后端接口的时候,就必然存在跨域问题 - 跨域是哪里的限制/谁的行为?
跨域只存在于浏览器(前端项目访问另一个服务器后端接口的时候) - 解决方式:
- 开发环境下:配置代理服务器,Vue配置 devServe.proxy 去实现开发环境下的代理
module.exports = { devServer: { // 代理配置 proxy: { // 这里的api 表示如果我们的请求地址有/api的时候,就出触发代理机制 // localhost:8888/api/abc => 代理给另一个服务器 // 本地的前端 =》 本地的后端 =》 代理我们向另一个服务器发请求 (行得通) // 本地的前端 =》 另外一个服务器发请求 (跨域 行不通) '/api': { target: 'www.baidu.com', // 我们要代理的地址 changeOrigin: true, // 是否跨域 需要设置此值为true 才可以让本地服务代理我们发出请求 // 路径重写 pathRewrite: { // 重新路由 localhost:8888/api/login => www.baidu.com/api/login '^/api': '' // 假设我们想把 localhost:8888/api/login 变成www.baidu.com/login 就需要这么做 } }, } } }
- 生产环境下:
- 借助**
Nginx
**的反向代理来进行(后端服务器实现) - CORS:后端服务器允许跨域,后端实现(两次请求:一次是OPTIONS请求,第二次是根据OPTIONS的预检结果确定是否跨域和如何跨域)
- JSONP:原理:利用script标签的src属性不受同源策略的限制,请求跨域的数据接口,并通过函数调用的形式,接收跨域接口响应回来的数据(只能发起GET请求)(不常用)
- 借助**
- 开发环境的跨域会影响生产环境吗?
- 不会,开发是开发,生产是生产:互相没有关系
5. GET 和 POST 的区别
- url可见性:
- get:参数可见
- post:参数不可见
- 数据传输:
- get:通过拼接url进行传递参数(查询字符串queryString)
- post:通过body体传输参数
- 缓存行:
- get:可以缓存
- post:不可以缓存
- 后退页面的方法:
- get:不产生任何影响
- post:需要重新提交请求
- 传输数据大小:
- get:一般数据的传输不超过2k-4k,根据浏览器的不同,限制不一样,但相差不大
- post:传输数据的大小根据配置文件设定
- 安全性:原则上post肯定比gte安全,毕竟参数不可见
6. HTTP 和 HTTPS的区别
- HTTPS需要申请证书,一般免费证书很少。因而需要一定的费用
- HTTP是超文本传输协议,信息是明文传输,HTTPS则是具有安全性的SSL加密传输协议
- HTTP和HTTPS使用的是完全不同的连接方式,用的端口不一样,HTTP默认端口是80,HTTPS默认端口是443
- HTTP的连接很简单,是无状态的。HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP协议更安全。
8. 一个页面从输入URL到页面加载显示完成,这个过程中后发生了什么?
- 浏览器查找域名对应的IP地址(先查是否有缓存,有缓存直接法请求)
- 浏览器向Web服务器发送一个HTTP请求(TCP三次握手)
- 浏览器301重定向
- 浏览器跟踪重定向的地址,请求另一个带www的网址
- 服务器处理请求(通过路由读取资源)
- 服务器返回一个HTTP响应
- 浏览器进DOM树构建
- 浏览器发送请求获取嵌在HTML中的资源(图片、音频、视频、CSS、JS……)
- 浏览器显示完成页面
- 浏览器发送异步请求
9. axios请求的data最终会出现在哪里?pararms会出现在哪里
data
:请求报文的body体params
:url请求地址(格式:queryString)
10. 说一说axios的拦截器原理及应用?
axios为开发者提供了这样一个API:拦截器。拦截器分为 请求(request)拦截器和 响应(response)拦截器。
- 拦截器原理其实:用use添加用户自定义的函数到拦截器的数组中,最后把他们放在拦截器请求前,请求后,组成promise链式调用。
- axios的拦截器的应用场景:
- 请求拦截器用于在接口请求之前做的处理,比如为每个请求带上相应的参数(token,时间戳等)。
- 响应拦截器用于在接口返回之后做的处理,比如对返回的状态进行判断(token是否过期)。
11. HTTP个版本的区别
截至到现在,IETF已经发布了5个HTTP协议了,包括HTTP0.9、HTTP1.0、HTTP1.1、HTTP2、HTTP3
HTTP0.9
- 没有
header
,功能非常简单,只支持GET
HTTP1.0
- 明文传输安全性差,
header
特别大,相比 0.9 有以下增强
- 增加了header(使用元数据与数据解耦)
- 增加了status code,用于声明请求的结果
- content-type可以传输其他文件
- 请求头增强了http/1.0版本号
- 缺点:每请求一次资源就新建一次tcp连接
HTTP1.1
- 使用最广泛的版本
- 可以设置keepalive让http重用tcp连接(请求必须串行发送)
- 支持pipeline传输,请求发出后可以继续发送请求
- 增强了HOST头,让服务端知道用户请求的是哪个域名
- 增强了type、language、encodeing等header
- 2014年更新了内容:
- 增加了TLS支持,既https传输
- 支持四种模型:短连接,可重用tcp的长连接,服务端push模型(服务端主动将数据推送到客户端cache中),websocket模型
- 缺点:还是文本协议,客户端服务端都需要利用cpu解压缩
HTTP2
- 2015发布
- 头部压缩(合并同时发出请求的相同部分)
- 二进制分帧传输,更方便头部只传输差异部分
- 流多路复用,同一服务只需要用一个连接,节省了连接
- 服务器推送,一次客户端请求服务端可以多次响应
- 可以在一个tcp连接中并发发送请求
- 缺点:基于http传输,会有对头阻塞问题(丢包防止窗口滑动),tcp会丢包重传,tcp握手延时长,协议僵化问题
HTTP3
- 2018发布,基于谷歌的QUIC。底层使用udp代码和tcp协议
- 这样就解决了对头阻塞问题,同样无需握手,性能大大的提升,默认使用tls加密
12. Axios是什么,有哪些特性和使用场景
- 定义:
- Axios是一个基于 Promise 的HTTP库,简单来说就是可以发送get、post请求。
- ajax的封装。
- 特性:
- 可以在浏览器中发送XMLHttpRequest
- 可以在node.js中发送http请求
- 支持 Promise API
- 请求拦截器和响应拦截器
- 转换数据请求和响应数据请求
- 能够取消请求
- 自动转换JSON数据
- 客户端支持保护安全免受XSRF攻击
- 使用场景:
- 浏览器、Node发送请求都可以用到Axios
- 像Vue、React、Node等项目都可以使用Axios。
陆、ES6
1. Set 和 Map的区别?
- Set:用于数据重组
- 成员不能重复
- 只有键值没有键名
- 可以遍历
Set:存放的是地址不同引用数据和值不同的基本数据
Set方法:
- 添加:
set.add()
- 已有一个空数组 / 对象(引用数据类型),在添加一个空数组,还可以添加(前提:这个空数组不被变量接收)
- 可以链式调用
- 清空:
set.clear()
- 没有返回值 - -不能链式调用
- 删除:
set.delete(数据)
- 返回值:布尔值
- 删除成功 - true (删除有的)
- 删除失败 - false(删除没有的)
- 返回值:布尔值
- 查:
set.has(数据)
- 返回值:布尔值
- Map:用于数据存储
- 本质上是键值对的集合
- 可以遍历,可以跟各种数据格式转换
数据映射结构
Map 和 Object 的区别:
- key:
- Map:可以是任意值
- Object:不能是数字、布尔值、对象……
Map方法:
- 读:
map.get()
- 写:
map.set()
- 清空:
map.clear()
- 查:
map.has()
- 删除:
map.delate()
2. 什么是回调地狱,怎么解决回调地狱?
- 回调地狱:多层回调函数的相互嵌套(回调函数嵌套的层数太多)
- 写法:虽然函数是同步的,在同步函数里面又有回调,继续这样写的话存在回调地狱
- 解决方式:
- Promise的 .then 方法链式调用
- async + await
3. Promise
- Promise是解决异步编程导致的地狱回调问题(异步编程不方便)
- Promise是一个容器,里面保存着异步操作的结果。从语法上来讲,Promise是一个对象,从他里面可以获取异步操作的结果
- 特点:
- Promise的状态不受外界的影响
- Promise对象代表一个异步操作,有三种状态,pending、fulfilled、reject,只有异步操作的结果才可以决定Promise当前是哪一种状态,其他任何操作都无法改变这个状态
- 一旦状态改变,就不会再变,任何时候都能获取异步操作的结果
- Promise对象的状态改变,只有两种可能:pending->fulfilled 或 pending -> rejected。只有这两种情况发生,状态就不会再变了,会一直保持这个结果。如果改变已经发生,你再对Promise对象添加回调函数,也会立即得到这个结果
- 缺点:
- Promise一旦创立,就会立即执行,无法中途取消
- 如果不设置回调函数,Promise内部抛出的错误无法反应到外部
- 当处于pending状态时,无法得到目前进展到哪一阶段
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
3. Promise有几种状态,什么时候进入catch?
-
Promise的三种状态:
1)pending — 进行中
2)fulfilled — 完成
3)reject — 失败
-
什么时候进入catch?
1)当pending为rejected时,会进入catch
4. Promsie.all() 和 Promise.race()的区别?
- Promise.all():发起并行的Promsie异步操作,等待所有的异步操作全部结束后才会执行下一步操作(等待机制)
- Promsie.race():发起并行的Promise异步操作,只要任何一个异步操作完成,就立即执行下一步操作(赛跑机制)
- all 和 race 的入参是个数组
5. Promise中reject和catch处理上有什么区别?
- reject用来抛出异常;catch用来处理异常
- reject是Promise的方法;catch是Promise实例的方法
- reject后的东西一定会进入then中的第二个回调,如果then中没有第二个回调,则进入catch
- 网络异常,会直接进入catch而不会进入then的第二个回调
-
const promise = new Promise(function(resolve, reject) { // ... some code if (/* 异步操作成功 */){ resolve(value); } else { reject(error); } }); Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。 resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。 Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。 ---------------------------------------------------------------------- promise.then(function(value) { // success }, function(error) { // failure }); then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数
6. async 和 await
- async和await是用来简化Promise的
- 只要函数内部使用了await,该函数就必须被async所修饰
- 当函数执行的时候,一旦遇到await就会先返回(相当于代码卡在了await所在的这一行),等到异步操作完成,再接着执行函数体后面的代码
- 第一个await之前的代码会同步执行,之后的代码会异步执行
7. 如何将同步函数转成异步函数?
- 在函数前面加上async,里面不变
async function test() { console.log(1) } test().then() //给同步函数加async修饰词,等价于下面的代码 function asyncTest() { return Promise.resolve().then(test) } asyncTest.then()
8. 如何将异步函数转成同步函数?
- acync + await
9. CommonJS 和 ES6模块化的区别?
-
CommonJS是同步加载,ES6模块化是异步加载
-
CommonJS模块输出的是一个值的拷贝,ES6模块输出的是值的引用
1)CommonJS:一旦输出一个值,模块内部的变化就影响不到这个值
2)ES6:JS引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值
-
CommonJS模块是运行时加载,ES6模块是编译时输出接口
1)CommonJS加载的是一个对象,该对象只有在脚本运行完才会生成
2)ES6模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成
柒、Vue
1. Vue最大优势是什么?
- 轻量级框架,简单易学,数据绑定,数据和结构分离,虚拟DOM,运行速度快,组件化;
- 文档都是中文的,入门教程很多,上手简单;
- Vue是单页面应用,使页面局部刷新,不用每次跳转页面都要请求所有的数据和dom,加快了访问速度和提升了用户体验;
- 相比传统的页面通过超链接实现页面的切换和跳转,Vue 使用路由不会刷新页面;
- 使用Vue编写出来的界面效果本身就是响应式的,这使网页在各种设备上都能显示出非常好看的效果;
- 第三方UI库组件库使用起来节省了很多开发时间,从而提升开发效率;
2. Vue和jQuery区别是什么?
- jQuery应该算是一个插件,里面封装了各种简单易用的方法,它的本质就是使用更少的代码直接去操作DOM,它是使用选择器获取DOM对象,随其进行赋值、取值、事件绑定等操作,对数据的操作依赖与对应的DOM对象;
- Vue是一套渐进式的框架,拥有自己的规则体系和语法,特别是MVVM的设计思想,让数据和试图进行双向绑定,极少操作DOM,对数据进行绑定不再依赖于响应的DOM对象,可以说数据和试图是分离的,他们通过Vue这个对象的VM实现相互绑定。
3. MVVM 和 MVC区别是什么?
- MVC:也是一种设计模式,是Model数据模型,View试图,Controller控制器,在控制器这层里面编写js代码,来控制数据和视图关联,MVC是单向通信;
- MVVM:既
Model-View-ViewModel
的简写,模型-视图-视图模型,VM是整个设计模式的核心,有两个方向,首先:模型转换为视图,将从后端请求回来的数据转化为网页,实现方式:数据绑定;其次:视图转换为模型,将网页转化为后端的数据,实现方式:监听DOM时间。这两个方向都是先了,我们就成为数据的双向绑定; - 区别:都是一种设计思想,主要就是MVC中的Controller演变成MVVM中的VM。MVVM主要解决了MVC中大量的DOM操作导致的页面渲染性能低,加载速度慢,影响用户体验。
4. Vue常用修饰符
- 事件修饰符
.stop
==> 阻止事件冒泡.prevent
==> 阻止事件默认行- .once ==> 程序运行期间,事件处理函数只执行一次
- .native ==> 原生事件
- 按键修饰符
.enter
==> 监测Enter键- .esc ==> 监测Esc键
- v-model修饰符
.number
==> 尝试 用 parseFloat转数.trim
==> 去除 字符串 首尾两侧 的 空白- .lazy ==> 内容改变并且失去焦点触发
.sync
修饰符
- 可以在子组件内部直接修改父组件的值
- 格式:
this.$emit('update:对应的属性名', 值)
5. 对Vue渐进式的理解
- 主张最少、自底向上、增量开发、组件集合、便于复用。
6. v-show 和 v-if的区别
- 共同点:都可以控制元素的显示和隐藏(视觉效果一样);
- 区别:
- 原理不同:
- v-show:本质就是通过 控制CSS中的
display: none;
进行隐藏; - v-if:动态的向 DOM树内 添加 或 删除 元素;
- v-show:本质就是通过 控制CSS中的
- 编译条件不同:
- v-show:都会编译,初始值为false。只是将
display
设置为none
,但他也会编译; - v-if:初始值为false,就不会编译;
- v-show:都会编译,初始值为false。只是将
- 性能不同:
- v-show:只编译一次,后面就是控制CSS;
- v-if:不停的销毁和创建实例,更耗性能;
- 同时使用v-show和v-if带来的性能问题:
- v-if的优先级高于v-show
- v-show:产生更大的首次加载消耗;
- v-if:产生更大的切换消耗
- 原理不同:
7. Vue指令
{{ 表达式 }} => 作用:将Vue数据属性直接显示在标签内
- 表达式:
- 方法调用
- 算术运算 或 三元表达式
- 对象.属性名
- ==字面量
v-bind:属性名=“表达式” => 作用:给标签属性 动态赋值
- 简写::属性名=“表达式”
v-on:事件名=“少量代码/函数名/函数名(实参)” => 作用:绑定事件
- 简写:@事件名=“”
v-model=“表达式” => 作用:将表单元素的value属性 和 Vue数据属性 进行 双向绑定
- 单个复选框:
- 准备一个布尔型的数据属性,此时,v-model和复选框的checked值绑定
- 多个复选框:
- 给复选框手动添加value属性
- 准备一个数组型的数据属性(不是数组的后果:一选都选,共用一个布尔值)
- 数组 和 复选框的value属性绑定
- 单选框:
- 需要手动添加value属性
- 同组单选按钮使用同一个vue数据属性
- 下拉菜单:
- v-model添加在select标签身上
- 和option标签的value属性绑定
- vue数据属性是字符型(一次只能选一个值)
v-show=“表达式” => 作用:控制元素的显示 和 隐藏
- 表达式 为 true => 元素显示
- 表达式 为 false => 元素隐藏
v-if=“表达式” => 作用:控制元素的显示 和 隐藏
- 表达式 为 true => 创建插入节点
- 表达式 为 false => 从DOM树上移除节点
v-for=“(值[, 索引]) in 目标结构” => 作用:循环列表
v-slot 插槽
8. 为什么避免v-for和v-if在一起使用
- Vue在处理指令的,v-for比v-if具有更高的优先级;
- 如果一起使用,虽然不会报错,但是性能回大打折扣;
- 比如:使用v-for循环生成n个元素,生成的每个元素身上都带有v-if,也就是说v-if就要执行n次
- 如果非要在一起使用该怎么办?
- 使用
templace
标签,构成父子关系(嵌套),符template些v-if,子template写v-for
- 使用
9. 数组更新有时候v-for不渲染
- 因为 Vue内部 只能检测 数组 顺序 位置 数量 的 改变;
- 如果是针对某个值被重新赋值或使用了不改变原始数组的方法,Vue是监测不到的;
- 针对这一点,有两种解决方案:
- 某个值被重新赋值:使用
this.$set(更新的目标结构, 改变元素的索引/对象的属性, 更新的值)
; - 使用不改变原始数组的方法:用得到的新数组替换原来的旧数组;
- 某个值被重新赋值:使用
- v-for更新时,是如何操作DOM的?
- 循环出新的虚拟DOM结构,和旧的虚拟DOM结构做对比,尝试复用,就地更新内容。
10. Vue中:key的作用,为什么不能用索引
- 当Vue用v-for正在更新已渲染过的元素列表时,它默认用“就地复用”的策略。如果数据项的顺序被改变,Vue将不会移动DOM元素来匹配数据项的顺序,而是就地复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。
- key:
- 作用:为了更高效的更新虚拟DOM
- 是给v-for循环生成标签颁发唯一标识的
- 为什么不能使用索引?
- 因为索引是连续的,如果删除其中一个会导致最后一个被删除
- 当我们再删除的时候,:key再根据数据来把新旧的DOM做对比,删除:key不存在的对应的标签
11. 真实DOM 与 虚拟DOM
- 真实DOM:documnet文档下每个节点/浏览器Elements审查元素的每个标签
- 虚拟DOM:
- 保存在内存中的JS对象
- 本质:保存DOM节点关键信息(节点标签、属性节点等)的一个JS对象
- 优点:提高DOM更新的性能(提高渲染速度),不频繁操作真实DOM
12. deff算法的比较机制
- 根元素变化:删除DOM树重新建立;
- 根元素未变,顺序改变:更新属性;
- 根元素未变,子元素改变:
- 按照key去比较;
- 如果没有key 或者 key是索引,尝试就地更新;
- key == id,新旧虚拟DOM做对比,共有的部分不发生变化,没有的就在对应的位置插入DOM节点;
- key的规范:
- 有id用id,没有id用索引;
- 一段唯一不重复的数字或字符串。
13. 自定义指令的方法有哪些?它有那些钩子函数?对应的有哪些入参?
- 注册指令
- 全局注册指令:在
Vue对象
的directive
方法里有两个参数,一个是指令名,一个是回调/对象
- 如果是个对象,在对象内部必须指定inserted方法
- 局部注册:
directives
- 参数:
el
:当前绑定指令的元素binding
:一个对象
vname
:指令名字(不包含v-)value
:执行绑定的值expression
:字符串形式的指令表达式- 钩子函数:
bind
:只调用一次,指令第一次绑定到元素时调用inserted
:被绑定的元素插入父节点时触发update
:所有组件的VNode更新时调用componentUpdate
:指令所在组件的VNode及其子VNode全部更新后调用unbind
:只调用一次,指令与元素解绑时调用
14. 封装组件的过程
- 组件提升了整个项目的开发效率,能够把页面抽离成相对独立的模块。解决了传统项目开发过程中的效率低、难维护、复用性差等问题
- 在
components
目录下,新建组件文件,根据组件的功能来命名- 根据业务需求,把页面中可复用的结构样式对应的JS,抽离到一个单独的.Vue文件中,实现复用
- 具体步骤:
- 封装组件
- 导入组件:
import 组件对象 from '组件路径'
- 注册组件:
- 全局注册:
Vue component('组件名', 组件对象)
- 局部注册:
export default { components: { 组件名: 组件对象 } } ES6规定,属性名 和 属性值 一样,可以直接省略 冒号属性值
- 使用组件:组件标签
15. 组件命名的规范
- ✔大驼峰命名法(每个单词的首字母必须大写) 或 ❌链式命名法(字母全小写并使用中划线连接)
- .vue文件名 = 组件对象名 = 组件名= 组件标签名
16. Vue组件中data为什么是个函数?
- 每个组件都是Vue实例
- 组件共享data属性,当data的值是同一个引用数据类型的值时,改变其中一个其他的都会受到影响
- 组件中的data写成一个函数,数据以函数返回值的形式定义,这样每复用一次组件,就会返回一份新的data,类似于给每个组件实例创建一个私有数据空间,让各个组件实例维护自己的数据
- 单纯的写成对象的形式,就会使得所有的组件实例共享一份data,就会造成一变全变的结果
17. Vue组件如何进行传值?
- 父传子:props机制
- 父组件内设置要传递的数据,在父组件内子组件标签上绑定一个自定义属性并把数据绑定在自定义属性上,在子组件添加
props
参数接受即可- 子传父:事件机制
- 子组件通过Vue实例方法
$emit()
触发绑定在父组件内子组件标签上的方法并且可以携带参数,在父组件内进行修改- 兄弟通信:
- 在src下新建一个EventBus.js文件(作为中间人),创建空白Vue实例并导出,信息发送方:使用
$emit
方法,信息接收方:$on
方法- 非关联组件传值:
Vuex
18. 组件中写 name 选项有什么用?
- 项目中使用
keep-alive
时,可以搭配组件name
进行组件的缓存- 使用插槽时,
name
属性可作为占位标签的名字,供template
使用- Vue-devtools调试工具里显示的组件名称是有Vue组件内部的
name
属性决定的
19. Vue.cli中怎样使用自定义组件
- 在
src/components
下新建需要的组件文件,script里面一定要默认导出对象(export default{}
)- 导入组件:在需要该组件的页面,使用ES6模块化的默认导入语法(
impor 组件对象名 from path
)- 注册组件:新增配置项
components
,在里面注册组件(组件名: 组件对象名
一般组件名和组件对象名相同)- 使用组件:在对应的位置使用组件,单双标签都可以,根据需求决定
20. Vue该如何实现组件的缓存?
- 为什么需要组件缓存?
- 在面向组件开发中,会把整个项目拆分成多个业务组件,按照需求对组件进行整合
- 存在组件频繁切换的问题,在这个过程中,组件的实例都是在不断的销毁和创建,很是消耗性能,并且如果需要该组件的数据的话,我们是获取不到的,所以需要对组件的状态进行缓存
- 怎样实现组件缓存?
- 使用
keep-alive
标签包裹需要被缓存的组件,会缓存不活动的组件实例,主要用于保留组件状态或避免重新渲染- 优点:提高渲染性能,提升用户体验
21. 谈谈对Vue生命周期的理解
- Vue实例从创建到销毁的整个过程,就是Vue的生命周期(四个阶段 + 八个钩子函数)
- 初始化阶段:
beforeCreate()
:此时,data数据和methods等方法还没有挂载到Vue实例身上(无法使用)created()
:data数据和methods方法已经挂载到Vue实例身上,可以使用data数据和methods方法,通常在这个钩子里发起Ajax请求- 挂载阶段:
beforeMount()
:将App.Vue中的所有标签编译完毕(只是编译完毕,还没有变成真实DOM)mounted()
:虚拟DOM变成真实DOM,此时可以获取DOM节点- 更新阶段:
beforeUpdate()
:data数据变化后更新,此时数据是最新的,但是DOM节点还不是最新的updated()
:当组件渲染完毕后,此时可以获取最新的DOM内容- 销毁阶段:
beforeDestroy()
:这一步,实例仍然完全可用destroyed()
:实例销毁后调用,该钩子被调用后,对应Vue实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁,通常在这个钩子函数里面销毁定时器
22. 第一次页面加载会触发那几个钩子函数?
beforeCreate
、created
、beforeMount
、mounted
- 第一次页面加载需要将data数据和methods方法挂载到Vue实例上,并且需要将虚拟DOM变成真实DOM
23. Vue组件中的定时器怎么销毁?
- 单个定时器:在data选项中声明一个数据属性,用来记录定时器返回的id值,在销毁阶段进行销毁
- 多个定时器:在data选项中声明一个对象,给每个定时器进行取名,然后一一映射在对象中,在销毁阶段循环销毁即可
24. 对Vue单向数据流的理解
- 单向数据流:从父组件到子组件的数据流向
- 子组件内部不能直接修改从父组件传递过来的值
- 父组件中的数据更新,子组件中的数据会自动更新,但是子组件的数据更新不会使得父组件中对应的数据更新,如果想要修改父组件中的数据,必须借助
$emit()
方法
25.对单页面、多页面应用的理解以及优缺点
- 单页面应用程序:指一个系统只加载一次资源,之后的操作交互、数据交互时通过路由实现、ajax来惊醒的,页面并没有进行刷新
- 场景:网易云音乐官网
- 优点:局部刷新,开发效率高,切换页面的时候不用获取所有的数据和dom节点
- 缺点:开发成本高(需要前端路由技术),首次加载会比较慢,不利于SEO优化
- 多页面应用程序:一个应用中有多个页面,页面跳转是整页刷新
- 场景:电商网站
- 优点:有利于SEO优化
- 缺点:切换页面的时候,需要重新获取所有的数据和dom节点
26. Vue中的路由模式
- hash模式:
- 有#
- #以及#后面的称为hash,用
window.localtion.hash
读取,对浏览器安全无用- 特点:前端访问,#后面的变化不会经过服务器
- 特点:hash虽然在URL中,但不被包括在http请求中
- 通过
onhashchange
事件,监听url的修改- histroy模式:
- 无#
- 视觉上更好一点
- 特点:正常的访问,后端访问,任意地址的变化都会经过服务器
- history采用H5的新特性;且提供了两个新方法,
pushState()
、replaceState()
可以对浏览器历史记录栈进行修改,以及popState
事件的监听到状态变更- 更换模式:在
new VueRouter()
里面,增加model
属性,属性值为hash
(默认值)或history
27. 你知道style上加scoped属性的原理吗?
- 什么是scoped?
- 在组件中,为了使样式私有化,不对全局造成污染,可以在
style
便签上添加scoped
属性,以标识它只局限于当前组件- 原理:
- 给当前组件添加
data-v
开头的8位随机哈希值的属性- Vue中的
scoped
属性的效果主要通过PostCSS转译实现:即PostCSS给当前组件内的所有标签添加一个唯一不重复动态属性,然后,给CSS选择器额外添加一个对应的属性选择器来选择该组件中的dom,这种做法就是样式的私有化
28. 请说出路由配置项常用的属性及作用?
path
==> 路径component
==> 路径相对的组件name
==> 命名路由children
==> 子路由的配置参数(路由嵌套)props
==> 路由解耦redirect
==> 重定向路由meta
==> 路由元信息
29. 编程式导航使用的方法以及常用的方法?
this.$router.push()
==> 路由跳转this.$router.replace()
==> 路由替换this.$router.back()
==> 后退this.$router.forward()
==> 前进
30. $route 和 $router的区别
- 在注册路由的时候,提供的两个全局对象
$toute
:路由信息对象,包括path、hash、query、params、fullPath、metched、name等路由信息参数,表示当前激活的路由对象$router
:为vueRouter
的实例,相当于一个全局的路由对象,里面包含有很多属性和子对象,如history对象,将常用的跳转链接就可以用this.$router.push()
会往history栈中添加一个新的记录,返回上一个history也是使用this.$router.go()
方法
31. query 和 params之间的区别
query
:
- 和path配合使用
- 接受参数的时候,使用
this.$route.query.属性名
params
:
- 和name配合使用
- 接受参数的时候:使用
this.$route.params.属性名
32. 路由传值的方式
- 声明式导航传参:
- 查询参:
- 字符串:直接在
<router-link>
标签的to
属性的path后面使用?
拼接,多个键值对之间使用&
隔开- 对象有
path
属性和query
属性,path
属性写路径,query
的属性值是个对象,里面写要传递导的参数(键值对的形式)- 接值:
this.$route.query.参数名
- 动态参:先对规则数组进行改造(使用
:
占位(/path/:参数名1/:参数名2……)
)
- 字符串:
<router-link to="/path/具体指1/具体指2……">
- 对象:需要给对应的路由命名(添加
name
属性),to
属性值写成键值对的形式,有name
属性和params
属性,params
属性值是个对象,属性名就是规则数组中的path的变量名,具体指要和参数名对应(有几个参数名就有几个具体指)- 接值:
this.$route.params.参数名
- 编程时导航传参:
router.push()
- 无参:直接传递path
- 查询字符串:和声明式导航传参一样
- 动态路由传参:和声明式导航一样
- 不同点:使用命名路由传参,刷新页面会报错
33. Vue-Router有哪几种路由守卫?
- 全局前置守卫:
beforeEach()
- 全局后置守卫:
afterEach()
- 全局解析守卫:
beforeResolve()
- 路由独享守卫:
beforeEnter()
- 入参:
to
:去哪里form
:哪里来next
:下一个,我执行完了,我没问题了,到你了next()
:通过next()
:跳到指定地址
34. 路由之间是怎么跳转的?有哪些方式?
<router-link to="需要跳转到页面的路径">
this.$router.push()
- 跳转到指定的url,并在history中添加记录,点击回退返回到上一个页面
this.$router.replace()
:
- 跳转到指定url,但是history中不会添加记录,点击回退到上个页面
this.router.go(n)
:
- 向前或向后跳转n个页面,n可以是整数也可以是负数
35. Vue-Router是干什么的,原理是什么?
Vue-Router
是Vue.js官方提供的路由插件,他和Vue.js深度集成,适用于构建单页面应用。Vue的单页面应用是基于路由和组件,路由用于设定访问路径,并将路径和组件映射起来,传统的页面应用,使用一些超链接来实现页面的切换和跳转。在Vue-Router单页面应用中,则是路径之间的切换,也就是组件的切换。- 路由模块的本质:就是建立起
path
和component
之间的映射关系(一种一一对应的关系)- 原理:更新视图但不重新请求页面,是前端路由原理的核心,目前在浏览器环境中,这一功能的实现主要有两种方式:
- 利用URL中的hash(“#”)
- 利用History interface在HTML5中新增的方法实现
36. watch、methods、computed的区别?
watch
- 侦听器
- 观察和响应Vue实例上的数据变动
- 写法:
- 简易写法:方法,检测的数据属性就是方法名,入参有两个
newVal + oldVal
- 完整写法:对象,键是需要侦听的表达式,检测的如果是个引用数据类型,要写
deep: true
属性,表示开启深度侦听;handler
方法有两个参数,newVal + oldVal
,引用数据类型,新值、旧值、原来的表达式三者相等methods
- 方法
- 将被挂载到Vue实例身上
- 方法中的this指向Vue实例
computed
- 计算属性
- 计算属性将被挂载到Vue实例身上
- 一个计算属性的值,依赖于另外的数据属性计算而来,当依赖发生变化的时候,计算属性也会发生变化
- 计算属性具有缓存性,基于依赖的值进行缓存,依赖的变量不发生变化,都直接从缓存中取结果;当依赖发生变化,函数会自动执行,并把最新的结果再次缓存
- 写法:
- 简易写法:方法,必须有
return
- 完整写法:对象,对象里面有两个属性,都是方法
set() + get()
,get
必须指定return
,使用计算属性的时候触发get,给计算属性赋值的时候触发set- 三者的加载条件不同
- methods:必须要有一定的触发条件才能执行,比如说:调用函数,事件处理函数
- watch:data或computed数据发生变化触发
- computed:依赖发生变化的时候触发
- 相同点:watch和computed的简易写法和methods的写法一样
- 不通点:
- 计算属性:衍生/加工得到的数据,充电在与计算得到新的结果,必须返回,不能写异步代码,带缓存
- 计算属性的值是通过
return
返回的,但是异步函数的返回值不是通过return
返回的- 异步操作的时候return没有意义
- 侦听器:目的在于侦听数据的变动,重点在与侦听,写异步代码
- 方法:重点在于封装复用,体现封装思想,大多数配合事件使用或封装可复用的代码
37. computed中的属性名和data中的属性名可以相同吗?
- 不能同名:不管是
computed
属性名 还是data
属性名 或者是props
属性名,最终都会被 挂载到 Vue实例 身上,这就相当于同一个对象内不能有重名属性名 - 如果重名了,最后面 的那个属性会生效
38. 什么是Vue的计算属性
- 一些值需要依赖于别的变量计算而来
- 写法:
- 简易写法:方法的形式,必须有
return
- 完整写法:对象的形式
- 有两个函数
set()
+get()
使用
计算属性触发get()
- 给计算属性
赋值
触发set()
- 优点:
- 依赖于数据,数据更新,处理结果自动更新
- 计算属性内部this指向Vue实例
- 在template调用时,获取数据,也可以使用set方法,给计算属性赋值
- 相较于methods,不管依赖的数据变不变,methods都会重新计算,但是依赖数据不变的时候,computed从缓存中获取,不会重新计算
- 计算属性不能写异步代码
- 它是通过
return
返回值传递参数的,异步操作的时候return
是没有意义的(异步函数的返回值都不是用return
返回的)
39. 为什么使用VueX?
- 项目比较大,以往的关系组件通信和EventBus在解决组建通信这方面就很麻烦,所以选用Vuex
- 状态的统一管理,实现数据的共享
- 实现非关联组件之间的通信
40. VueX的5个核心属性是什么?
- 1️⃣
modules
:模块化 - 2️⃣
state
:状态管理 - 3️⃣
mutations
:同步修改state状态(修改state状态必须通过mutations,入参:state、payload) - 4️⃣
actions
:异步修改state状态(将异步提交给mutations进行修改,入参:context、payload) - 5️⃣
getters
:相当于Vue.js的计算属性(入参:state、payload)
41. VueX的辅助函数
- 1️⃣
mapState
- 2️⃣
mapMutations
- 3️⃣
mapActions
- 4️⃣
mapGetters
42. 模块化的state、mutations、actions是注册到哪里的
namespaced: true
- 没有开启命名空间:注册到全局
- 开启命名空间:注册到局部
43. 对axios进行二次封装
// 导入axios import axios from 'axios' // 创建一个axios实例 - axios.create() const service = axios.create({ // 配置基地址 baseURL: '', // 请求超时 timeout: 5000 }) // 请求拦截器 service.interceptors.request.use() // 响应拦截器 service.interceptors.response.use() // 默认导出 export default service
44. 为什么需要Vuex持久化?
- vuex中的数据在页面刷新后就没有了,所以无法实现数据的持久化
- 要实现数据的长久保存,可以通过浏览器的本地存储能力实现
- 本地存储能力:
- cookie:安全性高、时效性、cookie是在浏览器和服务器之间来回传递的
- localStorage
- 所以,在vuex中的某些状态不能随着页面的刷新被清理的时候(页面刷新之后,token还需要保留着),就需要持久化
45. 数据劫持
- Object.defineProperty(对象, 属性, 描述符)
Object.defineProperty(对象, 属性值, { setter(){ // xxx }, getter(){ // xxx return xxx } })
读取属性:触发get
设置属性:触发set
描述符是个对象
46. Vue数据双向绑定的原理和缺点?
- Vue响应式指的是:组件中的data发生变化,立刻触发视图的更新
- 原理:Vue采用数据劫持结合发布者-订阅者模式的方式实现数据的响应式,通过Object.defineProperty来劫持数据的setter和getter,在数据变动的时候发消息给订阅者,订阅者收到消息后进行相应的处理。
- 通过原生js提供的监听数据的API,当数据发生变化的时候,在回调函数中修改DOM
- 响应式原理:获取属性值会触发getter,设置属性值会触发setter方法,在setter方法中调用该DOM的方法,比如说input输入框,改变值得时候,在setter里面监听输入框的input事件,将修改后的值重新赋给变量
- 缺点:
- 一次性递归到底开销很大,如果数据量很大,大量的递归调用会导致栈溢出
- 不能监听对象的新增属性和删除属性
- 无法正确的监听数组的方法,当监听的下标对应的数据发生改变时,它是监测不到的
47. Vuex的Mutatios和Action之间的区别是什么?
- 流程顺序不同:
- 响应视图 ==> 修改state,视图触发actions,actions提交给mutations
- 角色定位不同
- 基于流程顺序,二者扮演不用的角色
- mutations:专注于修改state,理论上是修改state的唯一路径
- actions:业务代码,异步请求
- 限制
- 角色不同,二者都有不同的限制
- mutations:必须同步执行
- actions:可以异步,但是不能直接修改state(必须提交给mutations)
48. 简述Vuex的数据传递流程
- 当组件进行数据修改的时候,我们需要调用dispatch来触发actions里面的方法
- actions里面的每个方法都有一个commit方法,当方法执行的时候会通过commit触发mutations里面的方法
- mutations里面的每个函数都会有一个state入参没这样就可以在mutations里面进行state状态的修改,当数据修改完毕后,会传递给页面,页面的数据也会发生变化
49.
50. webpack常见的loader
file-loader
:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件url-loader
:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去source-map-loader
:加载额外的 Source Map 文件,以方便断点调试image-loader
:加载并且压缩图片文件babel-loader
:把 ES6 转换成 ES5css-loader
:加载 CSS,支持模块化、压缩、文件导入等特性style-loader
:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSSless-loader
:将less代码转成Csseslint-loader
:通过 ESLint 检查 JavaScript 代码
51. 什么是Vuex?
Vuex是一个专门为Vue.js应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化
52. v-model语法糖的原理
-
先绑定value,再绑定input事件
<!-- :value ==> V 绑定 M --> <!-- @input ==> M 绑定 V --> <input type="text" :value="username" @input="username = $event.target.value"> <input v-model="username" type="text">
-
v-model原理:
1)v-model:将元素的value属性和Vue数据属性进行双向绑定
2)标签上绑定input事件,在input事件的事件处理函数内,将value属性的值赋值给vue数据属性
3)在标签上使用v-bind指令给value属性绑定Vue数据属性
- 他会根据控件类型自动选取正确的方法来更新元素
- 它负责监听用户的输入事件以更新数据
- v-model会忽略所有表单元素的value、checked、selected特性的初始值,而总是将Vue实例的数据作为数据来源,因此我们应该通过JavaScript在组件的data选项中声明初始化值
- 拓展:
- v-model的内部为不同的输入元素使用不同的属性并抛出不同的事件
- text和textarea元素使用 value属性和input事件
- checkbox和radio使用checked和change事件
53. 懒加载
- 为什么要是用路由懒加载?
- 为给客户更好的体验,首屏加载速度更快一些,解决白屏问题
- 懒加载定义:懒加载简单来说就是延迟加载或按需加载,即在需要的时候进行加载
- 路由懒加载:
() => import('路由组件路径')
,Vue Router只会在第一次进入页面时才会获取这个函数,然后使用缓存数据- 图片懒加载:
require('图片路径')
54. 如何对首屏加载实现优化?
- 把不常改变的库放到index.html中,通过cdn引入
- Vue路由和组件的懒加载
- Vue组件尽量不要全局注册
- 使用更轻量级的工具库
- 开启gzip压缩
55. webpack的打包流程
- ❗❗ 面试 总结:
- 执行
webpack
命令,找到webpack.config.js
配置文件,根据配置的入口,将所有和入口有关系的文件进行打包压缩,根据配置文件里指定的出口将代码输出到指定位置
- 执行
56. 谈谈对Vue的理解?
- Vue是一套用来构建用户界面的渐进式框架
- 渐进式的含义:主张最少、自底向上、增量开发、组件集合、便于复用
- Vue的核心特性:
- 数据驱动视图(MVVM):
- MVVM就是Model-View-ViewModel
- VM是整个是整个设计模式的核心,主要有两个方向
- 模型转换为视图:就是将后端请求回来的数据转换为我们所看到的网页,实现方式:数据绑定
- 视图转换为模型:就是将网页转换为后端的数据,实现方式:监听DOM事件
- 组件化:
- 优势:
- 低耦合:可以在保持接口不变的情况下,替换不同的组件来快速完成需求
- 调试方便:出现问题可以快速根据组件来排除问题
- 提高维护性:由于每个组件的功能不一样,并且组件在系统中是被复用的,对组件代码的优化,可以实现系统的优化
- 指令系统:
57. 说一说 Vue 中 $nextTick 作用与原理?
- Vue更新DOM时是异步执行的,在修改数据后,视图不会立即更新,而是等同一事件循环中的所有数据更新完毕后,再统一进行视图更新。所以修改完数据,立即在方法中获取DOM,获取的仍然是未修改的DOM
$nextTick()
的作用是:该方法中的代码会在当前渲染完毕后执行,就解决了异步渲染获取不到最新DOM的问题$nextTick()
的原理:$nextTick()
本质是返回一个Promise
58. 使用自定义指令解决图像加载失败的场景和原理
- 场景:用户头像,商品的展示图…
- 原理:
- 在自定义指令内部,对img标签的src属性进行赋值
- 将指令绑定的值赋值给src属性
- 优化:判断绑定指令的元素是否为img标签,如果不是,就不生效
59. Vue.use()的原理是什么?
- Vue的use方法用来安装Vue.js插件
- 如果插件是一个对象,这个对象必须提供install方法
- 如果插件是一个函数,这个函数就会被作为install方法
- install方法调用时,会将Vue全局对象作为参数传入
- 所以自定义的插件在定义install方法时,必须包含(至少)一个入参,第一个入参必须是Vue全局对象
60. Vue的缺点
- 有些数据只在当前组件使用,别的组件不使用,没要写成响应式的
- 对数组有缺陷,比如我改变数组中某个元素的值,它是监测不到啊
- 对对象有缺陷,比如我给对象的重新增加属性的时候,他不是响应式的数据
- 不利于SEO优化
- 首屏加载速度慢,加载时,将所有的css,js文件进行加载
- 不支持IE678
- 只有加了scoped之后才能防止样式污染,应该默认就防止,像小程序一样
61. 说一说Vue列表为什么要加key
为了性能优化 因为vue是虚拟DOM,更新DOM时用diff算法对节点进行一一比对,比如有很多li元素,要在某个位置插入一个li元素,但没有给li上加key,那么在进行运算的时候,就会将所有li元素重新渲染一遍,但是如果有key,那么它就会按照key一一比对li元素,只需要创建新的li元素,插入即可,不需要对其他元素进行修改和重新渲染。 加分回答 key也不能是li元素的index,因为假设我们给数组前插入一个新元素,它的下标是0,那么和原来的第一个元素重复了,整个数组的key都发生了改变,这样就跟没有key的情况一样了
62. 组件的基本封装流程
组件的流程
使用步骤:封装组件 + 导入 + 注册 + 使用
可以将组件看成一个函数,定义template+script+style,这时我的这个组件就基本封装完成,还需要的考虑的就是入参和出参
入参:我这个组件需不需要使用一些别的数据,需要的话,可以使用props
进行接收
出参:我这个组件有没有返回值,有的话需要触发$emit()
方法
63. 为什么我们在前端工程里常用 less、scss 写样式
- 结构清晰,便于扩展。可以方便地屏蔽浏览器私有语法差异。封装对浏览器语法差异的重复处理,减少无意义的机械劳动;
- 可以轻松实现多重继承。完全兼容CSS代码,可以方便地应用到老项目中。LESS只是在CSS语法上做了扩展,所以老CSS代码也可以与LESS代码一同编译
64. 谈一下JS中实现异步的方法
- callback
- 发布订阅模式(点击事件)
- Promsie对象
- ES6生成器函数
- async/await
65. 模块化和组件化的区别?
- 组件化:
- 就像一个个小的单位,多个组件可以组合成组件库,方便调用和复用,组件间也可以嵌套,小组件组合成大组件
- 组件化能提高开发效率,方柏霓重复使用,简化调式步骤,提升项目可维护性,便于多人协同开发
- 组件是资源独立的,组件在系统内部可复用,但是组件和组件之间可以嵌套
- 模块化:就像是独立的功能和项目,可以调用组件来组成模块,多个模块可以组合成业务框架
66. 你都做过那么Vue的性能优化?
- 尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher
- v-if和v-for不能连用
- 如果需要使用v-for给每个元素绑定事件时,可以使用事件代理
- SPA页面采用keep-alive进行缓存
- key保证唯一
- 使用路由懒加载、图片懒加载
- 使用cdn加载资源
67. 关于 keep-alive 说法
keep-alive
可以通过include
属性,匹配要进行缓存的组件- 当组件在
keep-alive
内被切换,他的activated
和deactivated
这两个钩子函数将会被执行- keep-alive自身不会被渲染成一个DOM元素,也不会出现在组件的父组件链中
- max属性 控制 最多可以缓存几个组件 实例,一旦这个 数字达到 了,在新实例被创建之前,已缓存的组件中 最久没有被访问 的实例 会被销毁
68. 路由守卫
- Vue路由守卫分为全局路由、单个路由守卫、组件内部路由
- 全局路由守卫的钩子函数有:
beforeEach()
- 全局前置守卫afterEach()
- 全局解析守卫beforeResolve()
- 全局后置守卫- 单个路由独享的钩子只有一个:
beforeEnter()
- 组件路由守卫相关的钩子:
beforeRouterEnter()
beforeRouterUpdate()
beforeRouterLeave()
69. Vue使用虚拟DOM节点的特点
- 虚拟节点可以理解成节点描述对象,它描述了应该怎样去创建真实DOM节点
- 虚拟DOM又是:渲染引擎操作DOM慢,js运行效率高,于是将DOM对比操作放在JS层,提高效率
- 提升渲染性能,虚拟DOM的优势不在于单次操作,而是大量、频繁的数据更新下,能够对视图进行合理、高效的更新
- 虚拟DOM是以JavaScript对象为基础而不依赖真实平台环境,所以使他具有了跨平台能力
70. 关于vue-lazyload的描述
- 组件中使用
vue-lazyload
时,v-lazy
代替v-bind:src
- 组件中使用
vue-lazyload
时,:key
可以不加,如果不加:key
属性,刷新页面时,可能由于key相同,图片不刷新vue-lazyload
指令可以实现图片的懒加载- 使用
vue-lazyload
时,扩展功能api中的attempt
代表尝试加载图片数量
71. 项目优化
- 减少http请求
- 减少DOM操作
- 使用JSON格式来进行数据的交换
- 高效使用HTML标签和CSS样式
- 使用CDN加速
- 将CSS和JS放到外部文件中引用,CSS放头,JS放尾
- 精简CSS和JS文件
- 研所图片和使用图片Sprite(精灵图)技术
- 注意控制Cookie大小和污染
72.首次加载白屏原因以及解决方案+ 白屏原因:
- 之前没有资源(html、css、js、图片等等)缓存,所以要从后台拉取,需要时间,这个时间比较长,就会出现白屏
- 解决方案:
- 使用CDN加速
- 开启Gzip压缩
- 服务端渲染
- 骨架屏
- 懒加载
73.Vue3和Vue2的区别
- 根节点:
- Vue2只能有一个根节点;
- Vue3可以有多个根节点;
- API:
- Vue2采用选项式API;
- Vue3既可以使用选项式API也可以使用组合式API;
- 生命周期函数:
捌、其他概念
1. 什么是微前端
- 概念:微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将Web应用由单一应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立运行,独立开发、独立部署。微前端不是单纯的前端架构或者工具,而是一套架构体系
- 使用微前端的优点:
- 简单、分离、松耦合的代码仓库
- 独立开发、独立部署
- 技术栈无关
- 遗留系统迁移
- 技术栈升级
- 团队技术成长
- 使用微前端的缺点:
- 子应用切换
- 应用相互隔离,互不干扰
- 子应用之间通信
- 多个子应用并存
- 用户状态的存储 - 免登
- 微前端常用技术方案
- 路由发布式微前端
- iframe
- single-spa
- qiankun
- webpack5:module federation
- Web Component
拾、项目打包注意事项
- vue-cli默认的打包路径是
/
开头的,必须修改为./
开头 - 自测没问题的话,还需要修改路由模式