个人的一些面试问题,记录一下以供参考和学习,欢迎大家交流。
1. H5有哪些新特性,增加了哪些标签
① 新增了语义化标签,如header
、footer
、nav
、section
、article
、 aside
、detailes
、summary
、dialog
…
② 增加了视频、音频、画布标签 video
、audio
、canvas
audio元素支持三种音频格式文件: MP3, Wav, 和 Ogg
video支持MP4, WebM, 和 Ogg
③ 增加多个新的表单Input输入类型以及 progress
进度条标签
输入类型 | 描述 |
---|---|
color | 主要用于选取颜色 |
date | 从一个日期选择器选择一个日期 |
datetime | 选择一个日期(UTC时间) |
datetime-local | 选择一个日期和时间 (无时区) |
邮箱地址 | |
month | 选择年和月 |
number | 输入一个数字(字母e 也可输入,是数学中一个常数) |
range | 一定范围内数字值的输入域 |
tel | 定义输入电话号码字段 |
time | 选择一个时间 |
week | 选择周和年 |
url | url地址的输入域 |
search | 用于搜索域 |
④ 新增本地存储 localStorage、sessionStorage
2. 本地缓存有哪些及特点,各自支持的最大值
特性 | 周期 | 大小 | 方式 | 作用 |
---|---|---|---|---|
sessionStorage | 页面关闭就清除 | 最大5M | 需要手动设置到请求体中 | 当前页面的一些信息需要暂时缓存下来 |
localStorage | 除非手动清除,不然永久存在 | 最大5M | 需要手动设置到请求体中 | 一些用户的习惯类的信息,如token(有销毁时间的),css,js等 |
cookie | 有效时间前后端都可以进行设置 | 最大4K | 每一次请求都会自动携带 | 携带token(有销毁时间的) |
注意:session 是会话级存储,服务端。
3. flex布局常用属性,垂直水平居中该怎么设置
使用 flex 布局首先需要在父元素上添加:display: flex;
flex-direction
:设置主轴的方向,默认值 row
(从左到右)、column
(从上到下)
justify-content
:设置主轴上的子元素排列方式,可选值:flex-start(默认值,从头部开始排列)、flex-end(从尾部开始排列)、center(在主轴居中对齐–水平居中)、space-around(平分剩余空间)、space-between(两边贴边,再平分剩余空间)
align-content
:设置侧轴上的子元素排列方式 (多行),可选值:flex-start(默认值,从侧轴头部开始排列)、flex-end(从侧轴尾部开始排列)、center(在侧轴的中间对齐–垂直居中)、space-around(平分剩余空间)、space-between(两边贴边,再平分剩余空间)、stretch(拉伸–设置子项元素高度平分父元素高度)
align-items
:设置侧轴上的子元素排列方式 (单行),可选值:flex-start(默认值,从侧轴头部开始排列-从上到下)、flex-end(从侧轴尾部开始排列-从下到上)、center(在侧轴的中间对齐–垂直居中)、stretch(拉伸)
flex-wrap
:设置子元素是否换行 默认值 nowrap 不换行,wrap (换行)
设置盒子垂直水平居中:
/*在父元素上设置以下样式*/
display: flex; /*设置为flex布局*/
justify-content: center; /*水平居中*/
align-items: center; /*垂直居中*/
4. div盒子水平垂直居中的方法
① 使用绝对定位
position: absolute;
margin: auto; /*设置水平居中*/
top: 0;
left: 0;
bottom: 0;
right: 0;
② 未知宽高的盒子利用 transform 属性
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
③ 利用 flex 布局,见 2
④ 已知宽高的盒子,利用绝对定位和margin负值(自身宽高的一半)
position: absolute;
width: 300px;
height: 300px;
top: 50%;
left: 50%;
margin-top: -50%;
margin-left: -50%;
background-color: pink;
5. 常见的视频格式有哪些
.mp4、.avi、.ogg、.flv、.rm、.wmv
(浏览器不支持)
6. 关于对原型链的理解和使用场景
① 原型的理解:我们创建的每一个函数都有一个prototype属性,这个属性是一个指针,指向一个对象。这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。简单来说,该函数实例化的所有对象的_proto_
的属性都指向这个对象,它是该函数所有实例化对象的原型。
② 原型链的理解:当我们使用一个对象的属性或者方法时,会先在构造函数中去查找,如果查找不到会到原型中去查找,因为这个原型对象又会有自己的原型,所以会继续到它的原型中查找,这样层层递进查找,如果最终查找不到则返回null,查找的整个过程就形成了原型链。
这也是为什么我们新建一个对象就能使用toString等方法原因。
③ 使用原型的场景:
当我们需要多个实例化对象使用到同一种属性或方法时,可以利用原型去进行添加;
如: 在vue项目中,可以在vue的原型上绑定一些自定义的属性或方法,方便全局vue的实例化对象直接进行调用;亦可以为数组类Array进行自定义扩展功能,方便使用等
7. 对class的理解
① class 类本质上就是一个函数,自身指向的就是构造函数
② es6 中使用关键字 class 和构造函数方法本身差异并不大,可以理解为一种语法糖
③ constructor
方法是类的默认方法,可以省略,通过关键字 new
创建对象实例时,自动调用该方法
④ 正常情况下,无论是通过函数构造器还是 class 关键字创建的实例,this 都指向实例本身
⑤ 类中声明的方法不能加 function
关键字
⑥ 可以通过原型 prototype
修改类方法和新增方法,还可以通过 Object.assign
方法为对象动态增加方法
8. H5怎么实现自适应的,用的技术栈及原理,rem和px转化值是怎么设定的
技术栈:flexible.js + rem + vscode插件cssrem
方案:根据设计稿与设备宽度的比例,动态计算并设置 html 根标签的 font-size
大小;
flexible.js 原理:把当前设备划分为10等份,但是不同设备下,比例还是一致的;
① 设置meta标签:
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
② 计算 rem 和 px 转化值:
(function(){
// 获取html根标签dom元素
var html = document.documentElement;
//拿到html的宽度, 例如打印出来是750
var hWidth = html.clientWidth
// flexible.js是划分为10等份 750/10 = 75 所以 1rem = 75px 划分15等份则除以15
html.style.fontSize = hWidth / 10 + "px"; // 10rem正好可以把屏幕占满
//
})()
③ 如果屏幕超过750px,可以通过媒体查询设置根标签最大字体大小,不会让页面超过750
<style>
@media screen and (min-width: 750px) {
html {
font-size: 75px !important;
}
}
</style>
9. 发送两次get请求,使用相同的请求参数,但因为浏览器有缓存机制获取到数据是一样的(实际数据发生了变化,应该是不同的),如何解决
get 请求是有缓存的,当使用相同的参数再次发送请求时,会从缓存中读取,为避免这种情况,我们可以在每次 get 请求的时候加上一个时间戳参数,保证每次获取的数据都是最新的。
10. vue生命周期函数,created和mounted的区别,如果在created中初始化可能会出现什么结果,如何解决
11个生命周期函数包括:beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destoryed、activated (缓存的组件被显示出来时触发)、deactivated (缓存的组件隐藏时触发)、errorCaptured (当子孙组件出错时会触发)
created 和 mounted 的区别:
在 created 中 data 初始化完成,但dom元素还没有渲染,无法对 dom 元素进行一些操作;
在 mounted 中已经完成挂载,页面成功渲染,可以获取到真实dom。
如果在 created 中对一些dom进行操作,为防止获取不到出错情况,可以使用 Vue.nextTick
(this.$nextTick 是在下次 DOM 更新循环结束之后执行延迟回调)。
11. vue中父组件和子组件的created、mounted执行顺序是什么,为什么
执行顺序:父 created > 子 created > 子 mounted > 父 mounted
如果有多个子组件:
① 父组件的 created 钩子执行结束后,依次执行子组件的 created 钩子
② 多个子组件的 created 执行顺序为父组件内子组件DOM顺序
③ 多个子组件的 mounted 顺序无法保证,跟子组件本身复杂程度有关
④ 父组件一定在所有子组件结束 mounted 钩子之后,才会进入自己的 mounted 钩子
渲染过程:父组件挂载完成一定是等子组件都挂载完成后,才算是父组件挂载完,所以父组件的mounted在子组件的mounted之后。
父组件和子组件的生命周期钩子函数执行顺序:
父 beforeCreated > 父 created > 父 beforeMount> 子 beforeCreated > 子 created > 子 beforeMount > 子mounted > 父 mounted
12. 计算属性 computed 的特性,和 watch 监听的区别
computed 是计算一个新的属性,并将该属性挂载到vue实例上,可以直接当成data使用,具有缓存性,只有在它的依赖发生改变时才会重新计算,computed 必须要有要给 return 值。
watch 用于监听数据变化,提供两个参数 (newValue, oldValue),当数据发生变化便会调用执行函数
使用场景区别:computed 适合一个数据被多个数据影响,而 watch 适合一个数据影响多个数据
13. 深度监听怎么使用,使用场景
使用:watch里面有一个deep
属性,默认值是 false
,代表是否深度监听
watch: {
obj: {
handler(newName, oldName) { //特别注意,不能用箭头函数,箭头函数,this指向全局
console.log('obj.a changed');
},
immediate: true, //刷新加载 立马触发一次handler
deep: true // 可以深度检测到 obj 对象的属性值的变化
}
}
使用场景:
① 如果 watch 监听的数据是个对象或者数组(复杂数据类型)时,对象或数组中的属性发生改变时,默认情况下监听是不会触发的;
② 开启深度监听模式:只要对象中的任何一个属性发生变化的时候,就会触发对应的逻辑
另外提示:
immediate
属性默认值是 false
,代表是否以当前的初始值执行handler函数,设为true
代表会刷新加载时,立即执行一次里面的handler
方法
14. keep-alive 的特点,如何缓存某些组件,activated 和 created 的差别是什么,是怎么执行的
作用:主要用于保留组件状态或者避免重新渲染。
使用场景:当两个组件被很频繁调用时,使用keep-alive
进行缓存,这样页面就会从缓存中快速渲染,而不是重新渲染。
缓存组件用法:使用 keep-alive
标签包裹组件,且 keep-alive
标签上有 include
属性,值为组件名称,用逗号分隔。
keep-alive
的 props:
include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
max - 数字。最多可以缓存多少组件实例。
使用 keep-alive
组件后会触发两个钩子函数:activated,deactivated。(在服务端渲染期间不被调用)
注意:这两个生命周期函数一定是在使用了keep-alive
组件后才会有的,否则则不存在。
当引入 keep-alive 的时候,页面第一次进入,钩子的触发顺序 created -> mounted -> activated,退出时触发deactivated。当再次进入(前进或者后退)时,只触发 activated。
事件挂载的方法等,只执行一次的放在 mounted 中;组件每次进去执行的方法放在 activated 中, activated 中的不管是否需要缓存都会执行。
15. 对动态路由匹配的理解,如何传递和获取参数
个人理解:动态路由就是把匹配某种模式下的路由映射到同个组件中,使用动态路由参数来实现,其本质就是通过 url 传参。
传递参数:通过 params
和 query
两种方式传递参数
1. params 方式
① 配置路由:使用冒号 : 绑定动态参数
//router.js中配置路由信息
const routes = [{
name: 'users',
path: '/users/:id',
component: Users
}]
② 路由跳转时可以参数使用字符串或者对象的方式
使用对象的方式,必须由 name 属性引入路由,不能用 path;属性名必须跟配置路由时的动态参数名一致。
<!-- usersId 在data中定义,值为1 -->
<router-link :to="`/users/${usersId}`">XXX</router-link>
<router-link :to="{name: 'users', params: {id: usersId}}">XXX</router-link>
也可以使用 $router 方式进行路由跳转
// 方法1
this.$router.push('/users/' + this.usersId)
// 方法2
this.$router.push({
name: 'users',
params: {id: this.usersId}
})
params 方式传参,对应传参后的 url 地址为:localhost:8080/users/1
③ 获取参数,使用 $route.params.id
2. query 方式
① 配置路由:正常配置即可,不需要绑定动态参数
② query 传参的方式只可以通过对象,不可以使用字符串
query 对象里面的属性名(如 id)可以随便起名
<router-link :to="{name: 'users', query: {id: usersId}}">XXX</router-link>
使用$router 方式跳转:
this.$router.push({
path: '/users',
query: {id: this.usersId}
})
query 方式传参,对应传参后的url地址为:localhost:8080/users?id=1
③ 获取参数,使用 $route.query.id
16. $router 和 $route 的区别
$router 是路由实例对象,包含了路由的跳转方法,钩子函数等;
$route 是路由信息对象,包括 path、params、hash、query、name等路由信息参数
总结:路由跳转使用 router
,获取参数使用 route
17. vue2.x的双向绑定原理,缺点是什么,如何解决,3.x使用了什么来代替
vue2.x 的双向绑定是通过数据劫持以及结合发布者-订阅者模式来实现的,利用了ES5的Object.defineProperty(obj, key, val)
劫持各个属性的 setter 以及 getter ,在数据变动时发布消息给订阅者,从而触发相应的回调来更新视图。
缺点:通过下标方式修改数组数据或者给对象新增属性,无法监听
解决:vue 内部通过重写函数解决该问题, Vue.set
用于向响应式对象上添加新 property。
vue3.0 版本中使用了 proxy(代理)来替代了 Object.defineProperty
,通过 new Proxy()
来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
与Object.defineProperty的对比优势:
-
可以直接监听对象而非属性
-
可以直接监听数组的变化
-
Proxy 有多达13种拦截方式,不限于apply、ownKeys、deleteProperty、has等等是
Object.defineProperty
不具备的 -
Proxy 返回的是一个新对象,可以只操作新的对象达到目的,而
Object.defineProperty
只能遍历对象属性直接修改
18. vue的组件通信方式,重点说一下 provide 和 inject
① 父传子
- 父组件通过属性绑定,子组件通过 props 接收;
- provide 和 inject
在父组件中通过 provide
提供变量,在子组件中通过 inject
来注入组件。不论子组件有多深,只要调用了 inject
那么就可以注入 provide
中的数据。而不是局限于只能从当前父组件的 props 属性来获取数据,只要在父组件的生命周期内,子组件都可以调用。
// 父级组件通过 provide 提供 'foo'
var Provider = {
provide: {
foo: 'bar'
}
}
// 子组件通过 inject 注入 'foo'
var Child = {
inject: ['foo'],
created () {
console.log(this.foo) // => "bar"
}
}
② 子传父
子组件使用 $emit 发送事件,父组件监听该事件;
// 子组件内部某函数
this.$emit('eventName', data)
<Father @eventName="handle"></Father>
③ 兄弟组件
使用 eventBus,通过 bus.$emit
发送事件,使用 bus.$on
监听事件;
import bus from '@/bus';
// 发送
bus.$emit('childa-message', this.data);
// 接收
bus.$on('childa-message', function(data) {
console.log('I get it');
});
19. 对MVVM的理解
MVVM 模式中 Model 负责存储页面的业务数据,View 负责页面的显示逻辑,VM 指的是 ViewModel,它通过双向的数据绑定,将 View 和 Model 的同步更新给自动化了。当 Model 发生变化的时候,ViewModel 就会自动更新;ViewModel 变化了,View 也会更新。
我了解过一点双向数据绑定的原理,比如 vue 是通过使用数据劫持和发布订阅者模式来实现的这一功能。
20. vue的虚拟DOM
虚拟 DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。
步骤是先用 JS 对象记录一个dom节点的副本,当dom发生更改时候,先用 虚拟dom进行diffff,算出最小差异,然后再修改真实dom。
虚拟 DOM 的优点:
① 具备跨平台的优势;
② 操作 DOM 慢,js 运行效率高,我们可以将 DOM 对比操作放在JS层,提高效率;
③ 提升渲染性能(虚拟 DOM 的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新)。
虚拟 DOM 的缺点:
① 代码更多,体积更大;
② 内存占用增大;
③ 小量的单一的 DOM 修改使用虚拟 DOM 成本反而更高,不如直接修改真实 DOM 快。
21. diff 算法
Vue 的 diff 算法是基于 snabbdom 改造过来的,仅在同级的 vnode 间做 diff,递归地进行同级 vnode 的diff,最终实现整个 DOM 树的更新。因为跨层级的操作是非常少的,忽略不计,这样时间复杂度就从O(n3)变成O(n)。
diff 算法包括几个步骤:
① 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
② 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
③ 把所记录的差异应用到所构建的真正的 DOM 树上,视图就更新了
22. vue-loader是什么
vue-loader
用于解析和转换 .vue 文件,提取出其中的逻辑代码 script,样式代码 style,以及 HTML 的模板 template,再分别把它们交给对应的 loader 去处理。
总之,vue-loader 的作用就是提取。
css-loader
:加载由 vue-loader 提取出的 css 代码;
vue-template-compiler
:把 vue-loader 提取出的 HTML 模板编译成对应的可执行的 JavaScript 代码,预先编译好 HTML 模板相对于在浏览器中再去编译 HTML 模板的好处在于性能更好。
23. Mixin (混入) 的理解
① 什么是Mixins
mixins
(混入),官方的描述是一种分发 Vue 组件中可复用功能的非常灵活的方式,mixins 是一个 js 对象,它可以包含我们组件中 script 项中的任意功能选项,如 data、components、methods 、created、computed等等。我们只要将共用的功能以对象的方式传入 mixins 选项中,当组件使用 mixins 对象时所有 mixins 对象的选项都将被混入该组件本身的选项中来,这样就可以提高代码的重用性,使你的代码保持干净和易于维护。
② 使用场景:
当我们存在多个组件中的数据或者功能很相近时,我们就可以利用 mixins 将公共部分提取出来,通过 mixins封装的函数,组件调用他们是不会改变函数作用域外部的。
③ Mixins 的特点:
- 方法和参数在各组件中不共享,虽然组件调用了 mixins 并将属性合并到自身组件中来了,但是其属性只会被当前组件所识别并不会共享,也就是其他组件无法从当前组件中获取到mixins中的数据和方法;
- 引入mixins后组件会对其进行合并,将mixins中的数据和方法拓展到当前组件中来,在合并的过程中会出现冲突。
④ Mixins 合并冲突
-
值为对象(components、methods、computed、data)的选项,混入组件时选项会被合并,键冲突时优先组件,组件中的键会覆盖混入对象的键;
-
值为函数(created、mounted)的选项,混入组件时选项会被合并调用,混合对象里的钩子函数在组件里的钩子函数之前调用。
24. data中为什么必须是函数
官方文档是这么描述的:
当一个组件被定义,
data
必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例。如果data
仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象!通过提供data
函数,每次创建一个新实例后,我们能够调用data
函数,从而返回初始数据的一个全新副本数据对象。如果需要,可以通过将
vm.$data
传入JSON.parse(JSON.stringify(...))
得到深拷贝的原始数据对象。
所以如果 data 是一个对象则会造成数据共享,在多次使用该组件时,改变其中一个组件的值会影响全部该组件的值。而如果是通过函数的形式返回出一个对象的话,在每次使用该组件时返回出的对象的地址指向都是不一样的,这样就能让各个组件的数据独立。
25. template 中为什么只有一个根标签
当我们实例化 vue 的时候,需要填写一个 el 选项,来指定我们的SPA入口,同时我们也会在body里面新增一个id为app的div,相当于为 vue 开启一个入口;
let vm = new Vue({
el:'#app'
})
----------------------------------
<body>
<div id='app'></div>
</body>
如果在template下有多个div,那么该如何指定这个vue实例的根入口?
为了让组件能够正常的生成一个vue实例,那么这个div会被自然的处理成程序的入口。
通过这个“根节点”,来递归遍历整个vue树下的所有节点,并处理为vdom,最后再渲染成真正的html,插入在正确的位置
那么这个入口,就是这个树的‘根’,各个子元素,子组件,就是这个树的‘枝叶’,而自然而然地,这棵‘树’,就是指一个vue实例了
Vue 2 的 Virtual DOM 机制是基于 Snabbdom 实现的。而引入 Virtual DOM(的目的之一)是为了提升渲染性能,这要靠 diff & patch 来实现。模板的灵活性相对于渲染性能被放到了次要的位置,因此 Vue 2 决定每个组件实例只对应一个 VNode 以提高 diff 效率。
Vue 3 完全重写了 Virtual DOM 机制,引入了其他机制来保证模板渲染性能,此时组件对应的 VNode 数量已经不是重要的问题,便自然支持了 Fragment(也就是多根节点)。
26. slot 插槽的理解
插槽是子组件提供给父组件使用的一个占位符,用<slot></slot>
表示,父组件可以在这个占位符中填充任何模板代码,填充的内容会替换子组件中的slot标签。
插槽分为 具名插槽、默认插槽、作用域插槽;
具名插槽:使用name属性为插槽命名,一个子组件中可以有多个插槽;
默认插槽:没有name属性的插槽;
作用域插槽:指带有参数的插槽,子组件提供给父组件的参数,只能在插槽内使用。
27. 为什么vue是单向数据流的,而v-model却是双向的
vue 的单向数据流,指的的数据只能从父组件向子组件传递,子组件无法改变父组件的props,如果想修改只能通过事件触发的方式,在父组件中修改;
vue 的双向绑定指的是 MVVM 的 VModel 层,如果修改了数据,视图也会跟着改变。而改变视图,数据也会跟着改变,它指的是数据和视图之间的关系;
v-model 只是 vue 提供的一个语法糖(v-bind:value 和 v-on:input),用于简化 input 等元素的数据绑定操作。
28. vue常用指令
v-text、v-html、v-pre
v-show、v-if、v-else、v-else-if
v-for、v-on、v-bind、v-model、v-slot
v-cloak(这个指令保持在元素上直到关联实例结束编译。和 CSS 规则如 [v-cloak] { display: none }
一起用时,这个指令可以隐藏未编译的 Mustache 标签直到实例准备完毕。)
v-once(只渲染元素和组件一次,可以用于优化更新性能)。