面试题vue
vue2
4.1
vue的设计模式
MVVM:Model-View-ViewModel
- M:数据层,负责业务处理,与服务器交互,可以理解为原生js
- V:视图层,用户展示层,将数据渲染成UI展示给用户,可理解为html
- VM:视图数据层,用来链接Model和View的,监听view层/Model层的内容变化,从而渲染视图/修改数据
- 重心放在:视图层和模型层,双向数据绑定
什么是模块化
模块化是将代码按功能拆分成独立、可复用的模块,通过导入/导出整合,提高代码组织性和维护性
举例:一个时间格式化的方法,需要在多个页面应用,就可以将这个方法封装出来,导出,在需要的地方引用即可获取结果
什么是组件化
组件化的核心意义就是解耦合,减少冗余,其优点:
- 提高可复用性:最大程度提高代码的可复用性,减少开发过程中重复代码的存在,优化整个代码风格
- 减少耦合度:一个由多个组件拼成的页面,各组件之间各司其职,互不干扰,能快速定位问题
- 提高可维护性:因为每个组件职责单一,且多方复用,所以只需要维护单个组件,即可全部生效
- 类似于我做了一个锤子,这个锤子不仅可以砸煤的时候用,砸钉子的时候也可以用,一个工具适用不同场景,这个工具就是组件
- 原则:按照功能板块划分、本着复用性原则、拆的越细越好(这样才能更好的实现复用)
组件分为:功能性组件、业务性组件
- 功能性组件:UI组件库已经提供的组件,如:按钮、表单、列表等
- 业务性组件:根据实际业务自己封装的组件,比如:导航栏等
什么是指令系统
指令系统:vue框架预设好可识别的标签内v-
开头的特殊属性:当model层数据发生变化时,将连带影响,响应式的作用于DOM,其核心在于绑定,常用指令:
- 条件渲染指令
v-if
- 显示渲染指令
v-show
- 列表渲染指令
v-for
- 属性绑定指令
v-bing
- 事件绑定指令
v-on
,符号:@ - 双向数据绑定
v-model
,符号::
vue跟传统开发的区别
vue通过操作数据,间接带动dom同步渲染
其他开发是获取dom,直接操作dom
vue与react对比
相同点:
- 都有组件化思想
- 都支持服务端渲染
- 都有虚拟DOM
- 都是数据驱动视图
- 都支持自己的native方案:vue的weex,react的react native
- 都有自己的构建工具:vue的vue-cli,react的Create react app
不同点:
- 数据流向不同:react推崇单向数据流,vue推崇双向数据流
- 数据变化的实现不同:react是不可变数据,vue是可变数据
- 组件化通信的不同:react是通过回调函数,vue子向父组件有两种方式:事件和回调函数
- diff算法不同:
- react用diff队列保存更新的dom,生成patch树,再统一更新DOM
- vue使用双向指针,边对比,边更新DOM
4.2
说说你对SPA的理解
SPA:single Page Application,单页面应用,整个项目只有一个html,多页面的效果实际都是在一个html中加载不同页面片段实现的效果。好处:
- 避免了页面跳转中的重新加载,无感切换
- 动态加载适当的资源,保留复用资源
- spa框架:
react、vue、angular
SPA和MPA的区别
1 | SPA | MPA |
---|---|---|
组成 | 一个html+多个页面片段 | 多个html |
刷新方式 | 局部刷新 | 整体刷新 |
url模式 | 哈希模式 | 历史模式 |
SEO搜索优化 | 难实现,可以用SSR改善 | 容易实现 |
数据传递 | 容易 | 通过url、cookie、localStorage传递 |
页面切换 | 速度快 | 每次都要重新加载 |
维护成本 | 相对容易 | 相对复杂 |
SPA的优缺点:
优点:
- 具有桌面应用的即时性,网站的可移植性、可访问性
- 用户体验好、快,改变内容不需要重新加载页面
- 良好的前后端分离,分工明确
缺点:
- 不利于搜索引擎搜索
- 首次渲染加载速度慢(东西多)
如何给SPA做SEO优化
seo不好的根本原因,是爬虫爬取不到关键字,也就是网页的内容,因为单页面的内容主要由js渲染出来,所以查不到
1.SSR服务端渲染
将组件或页面通过服务器生成html,再返回给浏览器
v-show和v-if的区别
用法相同,都是判断条件是否成立而显示隐藏
不同点:
- 控制手段不同
- v-show:通过操作display:none,来隐藏DOM,DOM元素不销毁
- v-if:直接销毁DOM
- 编译过程不同
- v-if:有相对于元素的创建和销毁的过程,会重新创建或销毁绑定的事件
- v-show:只是css层面的隐藏显示,其他不变
- 渲染条件不同
- v-if:在渲染阶段,false的元素直接不渲染
- v-show:会先渲染出来,false的元素display:none
- 性能消耗不同
- v-if更高的切换消耗,v-show更高的初始渲染消耗
v-show和v-if的使用场景
频繁切换使用v-show
,很少切换使用v-if
因为v-if开销大,但能彻底销毁元素
vue实例挂载的过程
beforeCreate
:数据初始化未完成,不能访问data、props等created
:数据初始化完成,可访问data、props,但没有挂载元素- 挂载方法是调用
vm.$mount
方法 - vue不允许将根元素挂载到body或html元素上,就是不能用body或html作为根元素
render
的作用主要生成vnode
_update
的主要功能是调用patch
方法,将vnode
转化为真实DOM
vue的生命周期
beforeCreate | 组件实例创建前 |
---|---|
created | 组件实例创建完成 |
beforeMount | 组件挂载之前 |
mounted | 组件挂载完成 |
beforeUpdate | 组件更新之前 |
updated | 组件更新完成 |
beforeDestroy | 组件销毁之前 |
Destroyed | 组件销毁之后 |
activated | keep-alive缓存的组件被激活时 |
deactivated | kepp-alive缓存的组件被停用时 |
errorCaptured | 捕获到子孙组件的错误时 |
应用场景:
beforeCreate | 一般初始化new Vue()实例时触发,做一些初始化任务 |
---|---|
created | 组件初始化完成,各种数据都可调用,常用于异步数据获取,如调用API |
beforeMount | 未执行渲染,dom未创建 |
mounted | dom渲染成功,用于获取访问数据和dom元素 |
beforeUpdate | 更新前,可用于获取更新前各种状态 |
updated | 更新后,所有状态已是最新 |
beforeDestroy | 销毁前,组件的方法,属性还能被调用 |
destroyed | 销毁后,所有方法属性都被释放 |
created和mounted的区别?
最大的区别,两者都能拿到数据,但created中dom对象还没挂载,初始化数据的修改建议放在created中,如果放在mounted可能导致dom的再次刷新,出现闪屏的情况
为什么v-if和v-for不能一起用
重点:v-for
优先级比v-if
优先级高
这个观点是在element渲染的过程中得出的,会先判断for属性,再判断if属性
这就导致,for循环会先执行,会先渲染所有循环标签,渲染完之后,再判断if,如果是false,再去删除
这将造成无用的性能浪费
**如何避免:**在v-if的子元素中嵌套v-for
SPA首屏加载速度慢怎么解决
速度慢原因:
- 网络延迟
- 资源过大
- 接口重复请求加载
- 加载脚本时,渲染内容堵塞
优化方案:
1.减少入口资源加载
-
router采用动态加载路由引用加载,只在应用到路由是才加载对应资源
-
routes:[ path: 'Blogs',name: 'ShowBlogs',component: () => import('./components/ShowBlogs.vue') ]
2.图片资源的压缩
如在线icon,雪碧图等,如图片webp替代png等
3.静态资源本地缓存
- http缓存,设置
Cache-Control
、Etag
、Last-Modified
标头
4.UI框架按需加载
-
//全局引用 import ElementUI from 'element-ui' Vue.use(ElementUI)//按需引用 import { Button, Input, Pagination } from 'element-ui'; Vue.use(Button) Vue.use(Input) Vue.use(Pagination)
5.组件重复打包
-
//在webpack的config.js中配置 minChunks: 3 //将引用超过3次的包抽离出来,放到公共依赖文件,避免重复加载组件
6.开启Gzip压缩
-
拆包后,再用g-zip做一下压缩:
-
cnmp i compression-webpack-plugin -D
-
再在webpack上配置:
-
const CompressionPlugin = require('compression-webpack-plugin')configureWebpack: (config) => {if (process.env.NODE_ENV === 'production') {// 为生产环境修改配置...config.mode = 'production'return {plugins: [new CompressionPlugin({test: /\.js$|\.html$|\.css/, //匹配文件名threshold: 10240, //对超过10k的数据进行压缩deleteOriginalAssets: false //是否删除原文件})]}}
-
为什么data是一个方法,而不是对象
避免数据污染
因为vue组件可能会创建很多个实例:
- 如果data是对象,那所有实例的data都指向这个data对象,导致数据污染
- 如果data是function,则每个实例都有自己的data,互不干扰,数据隔离
根实例对象可以是对象也可以是函数,根实例是单例
组件实例对象必须是函数,组件是n例
为什么vue中给对象加新属性,没有刷新页面?
vue2
是用==Object.defineProperty
==实现数据响应式
const obj = {}
Object.defineProperty(obj, 'foo', {get() {console.log(`get foo:${val}`);return val},set(newVal) {if (newVal !== val) {console.log(`set foo:${newVal}`);val = newVal}}})
}
-
根本原因:vue给所有data中的属性都添加了get、set方法,实现响应式,新添加的属性没有get、set方法,所以不是响应式数据
-
解决办法:
-
Vue.set(obj,propertyName,value)的方式新增属性
-
Object.assign:
-
this.someObject = Object.assign({},this.someObject,{newProperty1:1,newProperty2:2 ...}) //先合并再整体赋值给对象,因为someObject本身是响应式,所有属性会被重新赋值get、set方法
-
-
this.$forceUpdate():慎用,迫使
Vue
实例重新渲染
-
-
总结:
- 添加少量属性:用Vue.set()
- 添加大量属性:用Object.assign()
- 实在不行:用forceUpdate
- Vue3能直接添加新属性
vue中组件和插件的区别?
组件:一个.vue
文件都可以视为一个组件
优势:
1.降低耦合度:即拿即用
2.调试方便:可快速定位问题,一劳永益
3.提高可维护性:后期维护成本低,随时修改
插件:类似于UI组件库中的buttons,这种就是插件
注册方式不同:
- 组件:
Vue.component
() - 插件:
Vue.use(插件名字,{ /* ... */} )
,插件需要在实例new之前注册
应用场景:
-
组件
(Component)
是针对App
的业务模块,它的目标是App.vue
-
插件
(Plugin)
是针对vue功能模块,它的目标是Vue
本身
vue组件的通信方式有哪些
1.父子组件通信
2.兄弟组件通信
3.祖孙组件通信
4.非关系组件通信
常见的通信方案
- 通过
props
传输 - 通过
$emit
触发自定义事件 - 使用
ref
- EventBus
$parent
或$root
attrs
与listeners
Provide
与Inject
- Vuex
-
props传输:父传子
-
children中的props属性可拿到父组件应用子组件时身上带的标签属性
-
//Children props:{ // 字符串形式 name:String // 接收的类型参数 // 对象形式 age:{ type:Number, // 接收的类型为数值 defaule:18, // 默认值为18 require:true // age属性必须传递 } } //parent <Children name="jack" age=18 />
-
-
$emit:子传父
-
//Children this.$emit('add', good) ,//第一个参数是方法名,第二个参数是参数 //Parent <Children @add="cartAdd($event)" />
-
-
$refs:父拿子数据,获取子组件实例
-
this.$refs.children.name
-
-
EventBus:兄弟互传
-
创建一个公共方法bus,挂载到VUE的原型上,这样所有的实例都能调用到
-
bus内有两个方法:
$emit
和$on
- $emit理解为触发
- $on理解为监听
-
class Bus { constructor() { this.callbacks = {}; // 存放事件的名字 } $on(name, fn) { this.callbacks[name] = this.callbacks[name] || []; this.callbacks[name].push(fn); } $emit(name, args) { if (this.callbacks[name]) { this.callbacks[name].forEach((cb) => cb(args)); } } } // main.js Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上 // 另一种方式 Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能
-
-
$parent:兄弟互传,父辈或祖辈做桥接
-
//组件A this.$parent.on('add',fn) //组件B this.$parent.emit('add',123)
-
-
provide和inject:
-
在祖先组件定义
provide
属性,返回传递的值 -
在后代组件通过
inject
接收组件传递过来的值 -
//祖先组件 provide(){return{foo:'foo'} } //后代组件 inject:['foo']
-
-
vuex:存放共享变量
- 一个属性三个方法:
state、getter、mutations、actions
- state:类似data
- getter:类似于计算属性
- mutations:同步函数,操作state和getter
- actions:异步函数的调用,比如调用API获取数据
- 一个属性三个方法:
双向数据绑定
重点ViewModel他的职责是:
- 数据变化后更新视图
- 视图变化后更新数据
它的主要组成为:
- 监听器(Observer):监听所有数据的属性
- 解析器(Compiler):解析每个元素节点的属性,绑定对应的更新函数
对nextTick的理解
问题:因为vue中的更新是一个异步更新队列,我们在没有更新数据之前就去操作,拿到的是旧数据
nextTick的作用就是等待数据更新后能在nextTick中拿到最新的准确的结果
对mixin的理解
mixin的意思是混入,是一个包含data、components、computed、methods、props等全部属性的js文件
这个文件可以局部混入或全局混入到.vue文件中,在mixin文件中写的所有的方法都会生效
Vue.component('componentA',{mixins: [myMixin]
})
使用场景:当我们在开发过程中发现多处使用同一样功能的代码,这块代码可以提炼出来,放到js文件中,在需要的地方混入
我们在开发后台的时候,应用的上传功能,因为这个上传在拿到文件资源后上传到oss,但是这个上传之前还有很多操作去做,比如判断体积,判断格式等等,这些代码大部分类似,又不能写到组件里,我们就提出来,用mixin去做的混入,再用到的组件中去嵌入他,一劳永逸,比组件更灵活
对slot的理解
slot:插槽,开发者留个使用者自由定义的位置
v-slot的修饰符是:#
插槽三种使用方式:
-
默认插槽:直接在组件标签内写入
-
//子组件 <template><slot><p>插槽后备的内容</p></slot> </template> //父组件 <Child><div>默认插槽</div> </Child>
-
-
具名插槽:指定插槽名,分别插入
-
//子组件 <template><slot>插槽defalut的内容</slot><slot name="content">插槽content的内容</slot> </template> //父组件 <child><template v-slot:default>具名插槽</template><!-- 具名插槽⽤插槽名做参数 --><template v-slot:content>内容...</template> </child>
-
-
作用域插槽:子组件传给父组件自己的属性,给父组件使用
-
//子组件 <template> <slot name="footer" testProps="子组件的值"><h3>没传footer插槽</h3></slot> </template> //父组件 <child> <!-- 把v-slot的值指定为作⽤域上下⽂对象 --><template v-slot:default="slotProps">来⾃⼦组件数据:{{slotProps.testProps}}</template><template #default="slotProps">来⾃⼦组件数据:{{slotProps.testProps}}</template> </child>
-
4.3
你能说一下key的作用吗
key的应用场景:
-
v-for
-
用
+new Date()
生成的时间戳做key-
<Comp :key="+new Date()" />
key是给每一个vnode的唯一id,也是diff的一种优化策略,可以根据key,更准确, 更快的找到对应的vnode节点
-
对keep-alive的理解
Keep-alive
是vue内置的组件,主要的作用是缓存不活跃的组件,避免重复渲染
keep-alive
中的属性:
include
:包括,正则表达式,名称匹配的缓存exclude
:排除,正则表达式,名称匹配的不缓存max
:数字,最大缓存数量
基础用法:
//路由中设置是否keepAlive
{path: 'list',name: 'itemList', // 列表页component (resolve) {require(['@/pages/item/list'], resolve)},meta: {keepAlive: true,title: '列表页'}
}
//渲染处判断该路由是否需要缓存
<keep-alive><!-- 需要缓存的视图组件 --> <router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive><!-- 不需要缓存的视图组件 --><router-view v-if="!$route.meta.keepAlive"></router-view>
修饰符
修饰符是用于限定某些功能的快捷提示词:
表单修饰符
事件修饰符
鼠标修饰符等
- .stop:阻止冒泡行为
- .native:绑定原生方法
- .once:只执行一次
- .self:事件绑定在自己身上
- .prevent:阻止默认行为
- .caption:事件捕获
- .keyCode:监听特定键盘按下
- .right:右键按下
你有自定义过指令吗
有的,比如表单禁止重复提交(节流)、图片懒加载这种
核心是使用Vue.directive
,给自定义的指令绑定一个函数去做的,比如重复提交这个:
// 1.设置v-throttle自定义指令
Vue.directive('throttle', {bind: (el, binding) => {let throttleTime = binding.value; // 节流时间if (!throttleTime) { // 用户若不设置节流时间,则默认2sthrottleTime = 2000;}let cbFun;el.addEventListener('click', event => {if (!cbFun) { // 第一次执行cbFun = setTimeout(() => {cbFun = null;}, throttleTime);} else {event && event.stopImmediatePropagation();}}, true);},
});
// 2.为button标签设置v-throttle自定义指令
<button @click="sayHello" v-throttle>提交</button>
你用过过滤器吗
过滤器(filter
)是输送介质管道上不可缺少的一种装置
主要作用就是获得一些规范化的内容
比如用户名所有英文必须大写,这个让用户去规范有点麻烦我就可以用过滤器
//用法,在DOM里
{{msg|msgFormat}}
//方法,再script里
filters: {msgFormat: function (value) {if (!value) return ''value = value.toString()return value.charAt(0).toUpperCase() + value.slice(1)}
}
什么是虚拟DOM?
-
实际上它只是一层对真实
DOM
的抽象,以JavaScript
对象 (VNode
节点) 作为基础的树,用对象的属性来描述节点,最终可以通过一系列操作使这棵树映射到真实环境上 -
通过
VNode
,vue
可以对这颗抽象树进行创建节点,删除节点以及修改节点的操作, 经过diff
算法得出一些需要修改的最小单位,再更新视图,减少了dom
操作,提高了性能 -
createElement
创建VNode
的过程,每个VNode
有children
,children
每个元素也是一个VNode
,这样就形成了一个虚拟树结构,用于描述真实的DOM
树结构
你有做过axios的封装吗?
当然,首先,axios封装我理解就是模块化的一个展现
所有的请求都是基于XMLHttpRequest
服务发送http请求,axios也是基于这个开发的
封装的目的:一行代码就能拿到后端返回的结果
封装的方向:
-
拦截请求和响应:做一些统一操作,比如设置超时时间,设置请求头,状态码的返回、
-
axios.interceptors.request.use(config=>{},err=>{})//请求拦截 axios.interceptors.respose.use(response=>{},err=>{})//响应拦截
-
-
统一维护不同环境下的接口地址
-
封装请求方法
简单的axios
class Axios {constructor() {}request(config) {return new Promise(resolve => {const {url = '', method = 'get', data = {}} = config;// 发送ajax请求const xhr = new XMLHttpRequest();xhr.open(method, url, true);xhr.onload = function() {console.log(xhr.responseText)resolve(xhr.responseText);}xhr.send(data);})}
}
xhr的了解
xhr:xmlHttpRequest,是客户端向服务端发送http请求获取数据的服务
5个状态值:
- 0:未初始化,尚未调用open()
- 1:启动,已经调用open(),但尚未调用send()
- 2:发送,已经调用send(),但尚未接收响应
- 3:接收,已经接受大部分响应数据
- 4:完成,已经接收到全部响应
SSR是什么?
SSR:server-side-rendering:服务端渲染
vue项目的结构?
vue项目结构的规范化,能够使开发者达到一个群体共识
目录的设计应该遵循一些原则:
- 文件夹跟内部文件的语义一致性
- 单一入口/出口
- 就近原则,耦合度高的文件应放在一起
- 公共的文件引用应用绝对路径等
project| .env.production //环境变量| .gitignore //git配置| babel.config.js //| package.json //项目启动命令及项目配置| vue.config.js //vue层面的项目配置|--public //公共资源| index.html //出口| favicon.ico|--src|--components //公共组件|--button //按钮组件|index.js|index.less|index.vue|--pages|--login //登录页|index.js|index.vue|index.less|--router //路由配置|index.js|--services //接口api|--utils //工具文件夹|--store //vuex仓库
vue中权限怎么做?
- 接口权限:后端返回指定的错误码,做后续操作
- 按钮权限:每次登录都会返回一个权限功能列表,页面初始化时根据权限检索功能,没有权限的按钮统一做限制
- 路由权限:每次跳转都需要路由守卫
router.beforeEach()
去校验权限并做相应处理 - 菜单权限:服务器直接返回需要渲染的菜单,再进行对应权限的渲染
权限功能需要前后端协商一致前端尽可能的去控制,更多的需要后台判断
如何解决跨域?
跨域:跨域的本质是浏览器同源策略的一种安全手段
同源的定义:
- 协议相同
- 主机相同
- 端口相同
- 反之则被浏览器认为非同源,举措就是不要返回的内容
解决办法:
- cors
- proxy
- nginx反向代理
cors:跨域资源共享,通过服务端响应头的设置,使浏览器接受数据
set('Access-Control-Allow-Origin', '*');
set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
proxy:代理的方式,vue本地开发的方式,先代理到本地服务器,再传给前端,制造同源的假象
amodule.exports = {devServer: {host: '127.0.0.1',port: 8084,open: true,// vue项目启动时自动打开浏览器proxy: {'/api': { // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的target: "http://xxx.xxx.xx.xx:8080", //目标地址,一般是指后台服务器地址changeOrigin: true, //是否跨域pathRewrite: { // pathRewrite 的作用是把实际Request Url中的'/api'用""代替'^/api': "" }}}}
}
你怎样处理项目中的错误
先定位错误类型:
- 后端接口错误
- 代码中本身逻辑错误
最直观的是去观察控制台
如果是后端的去跟后端伙伴协调,前端定位错误产生的位置,再分析错误逻辑,找到解决办法
vue3的了解,跟vue2的区别
vue3创建的初衷:
- 利用新的语法特性(es6)
- 解决架构问题
vue3的优化:
- 速度更快
- 体积更小
- 更易维护
- 更接近原生
- 更易使用
vue3新特性:
- 组件支持多个根节点
- 使用
composition api
代替options api
- 响应式系统API变了:
ref、reactive、readonly、computed、watch、watchEffect
- 生命周期用法变了
- setup()作为入口,beforeCreate和created都在setup方法中实现
- 其他钩子函数都在基础上加了on开头
beforeDestroy
变成onBeforeUnmount
destroyed
变成onUnmounted
Vue3
vue3性能优化都有哪些?
-
虚拟dom的优化
-
vue2所有的dom节点都会生成虚拟DOM,静态节点+动态节点
-
//所有节点都会生成虚拟dom <template><div id="content"><p class="text">静态文本</p><p class="text">{{ message }}</p><p class="text">静态文本</p></div> </template>
-
静态资源的diff和循环是无用的,它无需修改
-
vue3根据以上问题做的优化:
- diff算法优化
- 静态提升
- 事件监听缓存
4.4
vue3为什用ProxyAPI代替defineProperty API
vue2中存在的问题
-
响应式用的
Object.defineProperty()
- 所有属性添加get、set方法
- 当key为对象时,在set方法中,递归执行get、set方法
- 在删除/添加时没有监听到
- 如果深层的嵌套对象关系,需要深层的进行监听
-
总结:
- 检测不到对象属性的删除和添加
- 数组
API
方法无法监听到,如:push、pop - 嵌套对象,需要深层监听,造成性能问题
proxy的优点
- proxy的监听针对是对象:解决了增删检测不到的问题
- get方法中加一层判断对象的嵌套:解决深层监听的问题
vue3的composition API和vue2的options API
optionsAPI:(选项式api),就是.vue文件,通过共同定义methods
、computed
、watch
、data
等属性,处理页面逻辑
Composition Api:组件根据逻辑功能来组织的,一个功能所定义的所有 API 会放在一起(高内聚,低耦合)
- 解释:所谓的碎片化,是指一个功能在vue2分布在data,method等多个模块中,而vue3推崇在一个模块中完成这些
比较:
- 逻辑组织
optionsAPI
:一个功能可能需要data、methods等多模块共同实现compositionAPI
:一个功能在一个方法中都能定义,实现高内聚
- 逻辑复用
optionsAPI
:用mixins,命名冲突,数据来源不明compositionAPI
:hook 函数,直接引用,数据来源清晰
- 总结:
- 逻辑组织和逻辑复用方面,compositionAPI优于optionsAPI
- 因为
Composition API
几乎是函数,会有更好的类型推断 Composition API
中见不到this
的使用,减少了this
指向不明的情况
什么是tree sharking?
tree sharking:是通过清除多余代码,再去打包,压缩打包体积的技术
主要做两件事:
- 判断哪些模块已经加载
- 判断哪些模块和变量未被使用或者引用,进而删除对应代码
通过TreeSahrking给我们带来的好处:
- 减少程序体积(更小)
- 减少程序执行时间(更快)
- 便于将来对程序进行优化(更友好)
ES6
4.4
Var、let、const的区别
es5中,在顶层声明的变量可以认做全局变量,js中是window
,node中是global
对象
var:es5
- var声明的变量会被提升
- var能对一个变量声明多次,比如
var a=1,var a=1
let:es6
-
let类似于var,但声明只在当前代码块生效
-
不存在变量提升
-
只要作用域中存在let,则不收外界影响
-
var a=1; function test(){a=10;//errorlet a }
-
-
相同作用域不能重复声明
const:es6
- 声明且赋值常量,声明后不能再修改
- 如果a已经被var和let声明过,则不能被const声明了
区别:
1.变量提升
var
会提升,let、const
不会提升
2.暂时性死区
var
因为提升没有暂时性死区,let、const
只有在声明后才能被获取
3.块级作用域
var
不存在块级作用域,let、const
存在块级作用域
4.重复声明
var
可以重复声明,let、const
不能重复声明
5.修改值
var、let
可以修改,const
不可以修改
Es6数组新增了哪些扩展?
-
...
展开运算符:将数组展开成列表-
console.log(1, ...[2, 3, 4], 5)//1 2 3 4 5 //在解构赋值和形参中,将作为剩余运算符 let [a,...b]=[1,2,3,4,5]// a=1 b=[2,3,4,5] const obj = {a: 1, b: 2}; let arr = [...obj];//TypeError: Cannot spread non-iterable object
-
2.只可以在有遍历器的对象中使用
-
新增构造函数:
-
Array.from():将类数组转为真正的数组:类似数组的对象和可遍历的对象,set、map
-
Array.from([1,2,3],(x)=>x*x) // [1, 4, 9] ,第二个值为操作函数
-
-
Array.of(1,2,3):将一组值转为数组
- 没参数:返回空数组
- 1个参数,生成长度为n的数组
- 2+个参数,生成参数数组
-
-
实例新增的方法:
- copyWithin()
- find()、findIndex()
- fill()
- entries(),keys(),values()
- includes()
- flat(),flatMap()
copyWithin(target,find,findIndex)
:复制指定成员到指定位置find()
:找出第一个符合条件的数组成员findIndex()
:找出第一个符合条件的数组成员的位置,如果没有符合的返回-1fill(content,start,end)
:填充值,可以规定从哪里开始填充,到哪里结束entires、keys、values
:获取数组的键或值keys()
是对键名的遍历、values()
是对键值的遍历,entries()
是对键值对的遍历
includes()
:判断数组中是否存在某个值flat(2)
:将数组扁平化,参数为扁平嵌套的层数
4.5
ES6对象新增了哪些扩展?
属性名简写
//属性名简写
let x=1;
let y={x}//等同于y={x:x}
//方法名简写
let 0 ={test(){return 1}
}
- 简写的方法不能作为构造函数
super关键字
this
关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super
,指向当前对象的原型对象
解构赋值应用
-
//在解构赋值中,未被读取的可遍历的属性,分配到指定的对象上面 let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }; x // 1 y // 2 z // { a: 3, b: 4 }
ES6中属性的遍历
5种方法:
for...in
:循环遍历对象自身的和继承的可遍历对象属性Object.keys(obj)
:返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名Object.getOwnPropertyNames(obj)
:返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名Object.getOwnPropertySymbols(obj)
:返回一个数组,包含对象自身的所有 Symbol 属性的键名Reflect.ownKeys(obj)
:返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举
对象新增的方法
-
Object.is(param1,param2)
:判断两个值是否相等,类似于全等,但区别是,NAN=NAN,+0!=-0 -
Object.assign(target,source1,source2)
:将2,3参数合并到target对象中 -
Object.setPrototypeOf()
:设置一个对象的原型对象-
const o = Object.setPrototypeOf({}, null);
-
-
Object.getPrototypeOf()
:获取一个对象的原型对象 -
Object.keys(obj)、values()、entries()
:都是获取对象键或值 -
Object.fromEntries()
:将一个键值对数组转为对象
4.7
Es6新增的set、map方法
一句话:Set
是一种叫做集合的数据结构,Map
是一种叫做字典的数据结构
共同点:集合字典都可以存储不重复的值
不同点:集合是以**[值,值]的形式存在,字典是以[键,值]**的形式存在
-
set的方法:
-
add()、delete()、has()、clear()
-
let s=new set() //add s.add(1).add(2).add(2)//最后一个2不会被添加,因为已经存在了 //delete s.delete(2)
-
-
用法:
-
扩展运算和Set相结合,实现数组去重
-
let arr=[1,2,2,3,4,4,4] arr1=[...new Set(arr)]//arr1=[1,2,3,4]
-
-
-
Map的方法:
Map
类型是键值对的有序列表,而键和值都可以是任意类型- size属性、set、get、delete、clear、has
ES6中的Promise
promise:翻译为承诺,是解决传统异步编程回调炼狱的方案
优点:
- 链式操作减轻了编码难度
- 可读性更高
三种状态:
pending
:等待中fullfilled
:已成功rejected
:已失败
实例方法:
-
then(resolvtion,rejection)
-
第一个参数是成功后执行的方法,第二个参数是失败后执行的方法
-
getJSON('/posts.json').then(function(posts) {// ... }).catch(function(error) {// 处理 getJSON 和 前一个回调函数运行时发生的错误console.log('发生错误!', error); });
-
-
catch(rejection)
- 等同于
then(null,rejection)
- 等同于
-
finally(fn):不管失败还是成功,都会执行的方法
构造函数的方法:
-
let p =Promise.all([p1,p2,p3])
:所有实例都返回成功或失败,才会进行下一步- 所有实例都为
fullfiled
,p为fullfiled - 只要有一个是
rejected
,p为rejected
- 所有实例都为
-
Promise.race([p1,p2,p3])
:率先改变状态的实例值返回 -
Promise.resolve('foo')
:将现有对象转为Promise,状态为resloved-
Promise.resolve('foo') // 等价于 new Promise(resolve => resolve('foo'))
-
当参数为Promise对象时,不改动,直接返回,状态为resloved
-
当参数为then对象时,转为Promise对象,立即执行
-
当参数为其他时,转为Promise对象,状态为resloved
-
当没有参数时,返回Promise对象,状态为resloved
-
-
Promise.reject()
:将现有对象转为Promise,状态为rejected-
Promise.reject('出错了') //等价于 const p = new Promise((resolve, reject) => reject('出错了')) //等价于 p.then(null, function (s) {console.log(s) });
-
async/await/generator
Generator 函数是 ES6 提供的一种异步编程解决方案
Async/await则是generator的语法糖,相当于会自动执行generator函数
区别:
- async/await、promise。都是用来处理一步操作的
generator
不是为处理异步创建的,他的作用可以对象迭代、控制输出等- promise相对于Generator、async更复杂
- async、await用法相对简洁,是处理异步的最终方案
es6中的module
module:模块,是能够单独命名并独立地完成一定功能的程序语句的集合
模块的作用:
- 代码抽象
- 代码封装
- 代码复用
- 依赖管理
常见的模块模式:
- commonJS(node)
- AMD(require)
- CMD(import/export)ES6
CMD的用法:
-
export
:用于规定模块的对外接口 -
import
:其他模块导入需要的功能 -
//导出方式export export { a,b,c } import {a} from '...' import * as abc from '...'//导出方式export defalut export default {a,b,c } import abc from '...'
javascript
4.7
js的数据类型
- 基本数据类型6种:
- number:不同进制表示55
55:十进制
、067:8进制
、0x37:十六进制
- string:
- boolean
- undefined
- null
- Symbol
- number:不同进制表示55
- 引用数据类型7种+其他:
- Object
- function
- Array
- Date
- Regexp
- Set
- Map等
存储的区别:
- 基础数据类型内容存放在栈中
- 引用数据类型内容存放在堆中,地址存放在栈中
数组常用方法
- 操作方法
push()
:尾部推入==(原)==pop()
:尾部弹出==(原)==unshift()
:头部推入==(原)==shift()
:头部弹出==(原)==splice(start,end)
:删除数组中多项,返回删除的项==(原)==slice(start,end)
:复制数组中的多想,返回复制的项==(新)==indexOf(param)
:返回参数所在位置,没有返回-1includes()
:返回数组中是否存在参数,布尔值find()
:返回第一个匹配的元素filter()
:返回所有匹配的元素
- 排序方法
- reverse():反转所有元素
- sort(fn):升序或降序所有元素
sort((a,b)=>a>b)
:升序sort((a,b)=>b>a)
:降序
- 转换方法
arr.join(' ')
:数组转换为字符串,以空字符串连接
- 迭代方法
arr.forEach((item,index)=>{})
:循环遍历arr.some()
:返回布尔值,只要有一个符合条件返回truearr.every()
:返回布尔值,所有符合条件才返回truearr.filter()
:返回符合条件的项arr.map()
:对数组每一项进行,返回最终生成的新数组
字符串常用方法
所有字符串的方法都不是改变原字符串,而是创建新副本
- 增:不改变原字符
str.concat('world')
:返回拼接好的新字符串,不改变原字符串
- 删:不改变原字符串
str.slice(start,end)
:删除字符串某区域内的字符str.substr(start,length)
:从start开始的length个字符串str.substring(start,end)
:从start开始,到end结束
- 改:不改变原字符串
str.trim()、str.trimLeft()、str.trimRight()
:删除前后空字符串str.repeat(n)
:将字符串重复n次,返回结果str.toLowerCase()、str.toUpCase()
:将字符串全部大写或小写
- 查:
str.indexOf(param)、str.lastIndexOf(param)
:返回字符首次/最后一次出现的位置str.charAt(n)
:获取n位的值str.startWith('foo')
:检查开头是否匹配foo,不匹配则返回falsestr.includes('foo')
:检查是否包含foo
- 转换:
str.split('+')
:以+号分割字符串,返回最终数组
- 模版匹配:参数可以是正则,也可以是字符串
str.match(/.at/)
:返回所有匹配成功项的数组str.search(/at/)
,返回匹配索引str.replace(/at/,'oao')
:替换匹配到的值为oao
js中的类型转换机制
显示转换:强制转换,执行转换的方法实现的转换
隐式转换:自动转换,比如执行加法运算时
-
显示转换:
-
Number()
:将任意字符转为数字-
Number('324') // 324 Number('324abc') // NaN Number('') // 0 空字符串转为0 Number(true) // 1 Number(false) // 0 Number(null) // 0 Number(undefined) // NaN Number({a: 1}) // NaN Number([5]) // 5 对象:通常转换成NaN(除了只包含单个数值的数组)
-
-
parseInt()
:逐个解析字符,遇到不能转换的字符就停-
parseInt('123b2')//123
-
-
String()
:任意类型的值转为字符串-
String(1)//'1' String(false)//'false' String(null)//'null' String(undefined)//'undefined' String({a:1})//'[Object object]' String([1,2,3])//'1,2,3'
-
-
Boolean()
:将任意值转为布尔值-
Boolean(0)//false Boolean(null)//false Boolean(undefined)//false Boolean('')//false Boolean({})//true Boolean([])//true Boolean(new Boolean(false))//true
-
-
隐示转换:运算过程中的转换
-
比较运算符:
==、!=、>、<
、if、while,等需要布尔值的地方 -
算数运算符:+、-、*、/、%
-
转为false的情况:
- 0、‘’、undefined、null、false、NaN
- 其他都为true
-
自动转为字符串:
-
加法如果一方为字符串,则先转为字符串
'5' + 1 // '51' '5' + true // "5true" '5' + false // "5false" '5' + {} // "5[object Object]" '5' + [] // "5" '5' + function (){} // "5function (){}" '5' + undefined // "5undefined" '5' + null // "5null"
-
除了加法,其他运算符都为转为数值计算
-
'5'-'2':3 '5'*2:10 null-1:-1 true-1:0 false-1:-1 undefined-1:NAN 'abc'-1"NAN
-
-
==
和===
的区别
==:模糊比较
===:精准比较
==:
- 简单类型的比较,先转为数值类型,再进行比较
- 简单类型与对象比较:对象转化成其原始类型的值,再比较
- 两个都为对象类型:则比较他们的引用地址
- null 和 undefined 相等
- 存在NAN则返回false
===:只有两个操作数在不转换的前提下相等才返回 true
- null、undefined与自身严格相等:
null===null
、undefined===undefined
深拷贝与浅拷贝
浅拷贝:只关注拷贝对象自身的属性,如果属性是对象,拷贝的是引用地址
- 浅拷贝现象的方法:
Object.assign()
arr.slice(),arr.concat()
- 拓展运算符
深拷贝:如果对象属性为基础类型,直接拷贝,如果对象属性为引用类型,递归拷贝
-
深拷贝现象的方法:
_.cloneDeep()
:lodashJSON.stringfy()
:会忽略值为:undefined/symbol/和函数
-
深拷贝手写:
-
function deepClone(obj,hash=new WeakMap()){if(typeof obj!='object') return obj;//非对象情况下返回if(hash.has(obj))return hash.get(obj); //处理循环引用if(obj instanceof Date){//处理日期对象let newObj=new Date(obj);hash.set(obj,newObj);return newObj;}if(obj instanceof RegExp){//处理正则let newObj=new RegExp(obj);hash.set(obj,newObj);return newObj;}let newObj=Array.isArray(obj)?[]:{};//处理数组和对象hash.set(obj,newObj);for(let key in obj){ //递归遍历对象if(obj.hasOwnProperty(key)){newObj[key]=deepClone(obj[key],hash);}}return newObj; }
-
闭包是什么
闭包:闭包让你可以在一个内层函数中访问到其外层函数的作用域
闭包的特点:因为一个函数的存在,导致其父作用域的变量不能被回收
-
创建私有变量
-
延长变量的声明周期
-
function makeSizer(size) {return function() {document.body.style.fontSize = size + 'px';}; } let p1=makeSizer(12) document.getElementById('size-12').onclick = p1;
柯里化函数:避免频繁调用具有相同参数函数的同时,又能够轻松的重用
// 我们可以使用闭包柯里化这个计算面积的函数
function getArea(width) {return height => {return width * height}
}
let getAreaFromWidth=getArea(10);
let area1=getAreaFromWidth(20);
let area2=getAreaFromWidth(30);
你对作用域链的理解
作用域:即变量和函数生效(能访问到)的区域
作用域分为:
-
全局作用域:不在函数和块级作用域内声明的变量,都是全局作用域
-
函数作用域:function函数中声明的变量
-
块级作用域:花括号中声明的变量,var声明的变量因为会提升,所以会作为全局变量
-
let a=10//全局变量 //函数作用域 function(){let num=20//私有变量} //块级作用域 {const num=111;//局部let num2=222;//局部var num3=333;//全局 }
作用域链:当使用一个变量时,当前作用域找不到的话,会像父级作用域寻找,如果没有继续像上寻找,直到全局作用域,都没有就报错,逐级向上,就近原则
原型和原型链
原型:所有对象都有prototype属性,函数即是对象,也是函数
原型链:所有实例都有__proto__
,指向其构造对象的prototype属性
- 构造函数即存在
__proto__
,也存在prototypePerson.proto==Function.prototype
:函数是由Function构造器构造的Person.prototype.proto==Object.protptype
:prototype本身是一个对象,所以原型链指向Object的prototype
- 一切对象都是继承自
Object
对象,Object
对象直接继承根源对象null
- 一切的函数对象(包括
Object
对象),都是继承自Function
对象 Object
对象直接继承自Function
对象Function
对象的__proto__
会指向自己的原型对象,最终还是继承自Object
对象