面经

241 阅读25分钟

MVVM MVC MVP

MVVM MVC MVP是三种软件设计架构模式,主要目的是组织代码结构,优化开发效率。

MVC (Model-View-Controller)
  • view是视图层,展示布局文件
  • controller:逻辑层
  • model存储和处理数据

缺点:1. view对model的依赖导致view中也包含了业务逻辑 2. controller过于厚重

MVVM(Model-View-ViewModel) MVC的改进版
  • Model:模型层,负责制处理业务逻辑以及与服务器端交互;
  • View:视图层,负责将数据模型展示出来;
  • ViewModel:视图模型层,是公共属性和命令,用来连接view和model,是他们之间的通信桥梁;

viewModel和model之间是双向数据绑定的关系,model中数据改变会触发view的更新,view中用户操作数据引起的数据改变也会在model中同步。这种模式实现了model和view数据的自动同步,因此开发者只需要去维护数据即可,不需要去操作dom

优点:
  1. 低耦合(视图和业务逻辑分开)
  2. 可重用性高(可以把视图逻辑放在一个viewmodel中,让view去重用视图逻辑)
  3. 能实现独立开发(开发人员可以专注于业务逻辑和数据的开发)

computed和watch的区别

  • 功能上:computed是计算属性,watch是监听一个值的变化,然后执行对应的回调。

  • 是否调用缓存:computed中的函数所依赖的属性没有发生变化,那么调用当前的函数的时候会从缓存中读取,而watch在每次监听的值发生变化的时候都会执行回调。

  • 是否调用return:computed中的函数必须要用return返回,watch中的函数不是必须要用return。

  • computed默认第一次加载的时候就开始监听;watch默认第一次加载不做监听,如果需要第一次加载做监听,添加immediate属性,设置为true(immediate:true)

  • 使用场景:computed----当一个属性受多个属性影响的时候,使用computed-----购物车商品结算。watch–当一条数据影响多条数据的时候,使用watch-----搜索框.

单页面应用

单页面应用是只有一张web页面的应用,浏览器一开始就会加载必须的html、css和js,所有的操作都由js控制,都在这张页面上面完成。 优点:

  1. 具有桌面应用的即时性,网站的可移植性和可访问性
  2. 用户体验好,内容改变不需要重新加载整个页面
  3. 前后端分离,分工更明确 缺点:
  4. 不利于搜索引擎的抓取(搜索引擎只认识html里的内容,而单页面的内容都是靠js渲染生成处理的,搜索引擎识别不了这部分的内容,所以导致网页排名较差)
  5. 首次渲染速度较慢

如何解决首屏加载速度慢的问题?

响应式原理

  • vue2

vue会遍历data中的每个属性,把他们转换成setter和getter的形式,从而能够追踪属性的变化。当属性被读取时,会触发gettter,当前组件示例的watcher就会被添加到依赖手机列表中,当属性改变的时候会触发setter,从而通知watcher去进行相应的更新操作,进而更新视图。

缺点:

  1. 不能侦测到对象属性的添加和删除
  2. 利用索引修改数组元素,或者直接修改长度时,无法侦测到。 -vue3-----------------------------

vue生命周期

声明周期描述
beforeCreated组件初始化之后立即调用
created组件实例化完成,此时响应式数据、方法等都可以使用
beforeMounted组件挂载之前,dom还未创建
mounted组件挂载完成,此时可以访问dom元素
beforeUpdate组件因为一个响应式状态变更而引起dom更新之前前
updateddom更新后
beforeDestroy在组件被销毁之前,这个时候可以去清除组件内的定时器
destroy实例解绑,监听器移除了,组件已销毁
父子组件的生命周期
  1. 加载渲染过程 父beforeCreated -> 父created 父beforeMount -> 子beforeCreated -> 子created -> 子beforeMount -> 子mounted -> 父mounted
  2. 更新过程 父beforeUpdate -> 子beforeUpdate ->子updated -> 父updated
  3. 销毁过程 父beforeDestory -> 子beforeDestory -> 子destoryed -> 父destoryed

为什么v-for和v-if不能一起使用?

源码中先判断v-for再判断v-if,因此v-for比v-if优先级高,每次渲染都要先循环再判断,性能方面消耗较大。 解决:可以在循环的外层添加template标签,在template标签上进行判断

为什么data是个函数?

如果组件中的data是对象,那当组件被多次复用时,组件实例会引用同一个data对象,也就是共享一块内存地址,他们内部的数据会相互影响。而当data是函数的时候,函数会返回一个全新的data,不会共享内存。

因此data是个函数可以避免数据污染。

组件通信方式

bash
复制代码
父子组件通信:
$ref prop $emit vuex $attrs $listeners $parent provide inject $children v-model.sync 插槽
隔代通信:vuex provide inject $attrs $listeners
兄弟通信:vuex

介绍一下vuex

vuex是专为vue.js应用程序开发的状态管理插件,核心是store,它采用集中式存储管理应用的所有组件的状态,更改状态的唯一方法是提交mutation.

什么时候用vuex?
  • 多个组件依赖于同一个状态
  • 不同组件的行为需要变更同一个状态
vuex中的5个核心属性:
  • state: 存储vuex中的状态,使用MapState结合对象的展开运算符批量使用state

  • getters: 类似于计算属性,getter的返回值具有缓存特性,只有当依赖发生变更时才会重新计算。

  • mutations:更改state的唯一方法。相当于事件,每一个mutation都有一个事件类型和回调函数,但是它的回调函数必须是同步操作。

    • 可以直接更改store中的状态state
    • 只能执行同步操作
    • store.commit()进行提交
    • 第一个参数的state
  • actions

    • 提交的是mutation
    • 能够包含异步操作
    • store.dispatch进行提交
    • 第一个参数是context
  • modules:使用单一状态树时,所有的状态都会集中到一个比较大的对象上,store就会变得很臃肿,为了解决这个问题,可以将store分割成module,每个module都有自己的state,getters,mutatins,actions.

vuex和单纯的全局对象有什么不同?
  • vuex的状态存储是响应式的,当组件从store中读取状态时,若对应的状态发生了改变,相应的也会更新到视图。
  • 不能直接改变store的状态,只能通过mutation进行显示的提交,这样有利于跟踪状态的变化。

vue路由

router-link: 使用router-link来导航,to属性值是导航的地址。能够在不重新加载页面的条件下更改url. router-view: 渲染url对应的组件。

  • route和route和route和router的区别

route是路由信息对象,可以从中获取name,query,params等。route是路由信息对象,可以从中获取name,query,params等。 route是路由信息对象,可以从中获取name,query,params等。router是vue路由实例,一般用来进行页面跳转。

  • 路由重定向

路由重定向通过路由配置完成,使用redirect设置重定向的地址,它是值可以是路径,命名的路由或者一个方法

arduino
复制代码
//路径
const router = new VueRouter({ routes: [ { path: '/a', redirect: '/b' } ] })
arduino
复制代码
//名称
const router = new VueRouter({ routes: [ { path: '/a', redirect: { name: 'foo' }} ] })
csharp
复制代码
//方法
const routes = [ { // /search/screens -> /search?q=screens 
path: '/search/:searchText', 
redirect: to => { // 方法接收目标路由作为参数 // return 重定向的字符串路径/路径对象 
return { path: '/search', query: { q: to.params.searchText } } }, 
}, 
  • 动态路由

路径参数用:表示。当一个路由被匹配时,他的params值可以通过this.$route.params获取。路径参数可以是多个。

当路由参数变化时,虽然路径不同,但渲染的组件是同一个,因此组件的生命周期钩子不会被调用。如果需要在路由变化时完成某些操作,则需要用watch去监听params

  • 路由传参
  1. 布尔模式: 当该路由项的props为true时,对应的params将被设置为组件的props
  2. 命名视图:不同组件对应不同的props,值为布尔值。
  3. 对象模式:props对象中的属性会被原样设置为组件的props
  4. 函数模式:返回props,可以对参数进行转换。
  • hash和history
  1. hash模式
  • 什么是hash模式?

创建路由实例时,用createWebHashHistory()创建的,设置history的值为,这种模式下url中包含一个#号,#后的哈希值变化时,浏览器并不会重新发起请求,而是会触发onhashchange事件。

  • hash模式的特点

    • hash模式时只能改变#后面的值,因此只能跳转到与当前url文档同文档的url
    • hash模式通过onhashchange来监听路由的变化,借此实现页面的无刷新跳转
    • hash永远不会提交到服务器端
    • hash值每改变一次就会在history中增加一条历史记录。
    • hash值改变不会触发页面重新加载,所有的操作都是纯客户端的,因此这种模式不利于SEO。
  1. history模式
  • 什么是history模式? 创建路由实例时,用createWebHistory()创建的,通过调用window.history对象上的pushStatereplaceState方法实现页面的无刷新跳转。 history api是H5提供的新特性,允许更新浏览器地址而不用重新发起请求。

  • history模式的特点

    • 新的Url可以跟当前url一样,但是会把重复的一次操作记录到栈中
    • 通过pushState和replaceState实现无刷新跳转功能
    • 可以额外设置title属性,以供后续使用
    • 可通过history.state把任意类型的数据添加到记录中
  1. abstract: 非浏览器环境下使用
使用history模式的路由会有什么问题?

对当前页面进行刷新时,如果nginx没有匹配到当前的url,就会出现页面404的问题。

解决办法: nginx配置,把所有的请求都转发到Index.html上

路由懒加载

把不同路由对应的组件划分成不同的代码块,在路由被访问时才去加载对应的组件。

路由配置中component的值是一个返回promise组件的函数,只有在第一次进入页面的时候才会获取这个函数,然后使用缓存数据。

javascript
复制代码
// 将
// import UserDetails from './views/UserDetails.vue'
// 替换成
const UserDetails = () => import('./views/UserDetails.vue')

const router = createRouter({
  // ...
  routes: [{ path: '/users/:id', component: UserDetails }],
})

修饰符

  • .stop:阻止冒泡
  • .prevent:阻止默认事件
  • .number:转为数字类型
  • .trim:去除收尾空格
  • sync:实现prop的双向绑定(相当于:update, 子组件:this.$emit())

v-if vs v-show

v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。

v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

相比之下,v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。

一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。

nextTick

为什么会有nextTick?

当检测到数据变化时,不会立即更新dom,而是会开启一个任务队列,并且会缓存同一事件循环中的所有数据变更。这样可以将多次数据更新合并成一次,减少对Dom的操作. 这种策略导致数据的变更不会立即体现到dom上,如果想要获取更新后的dom的状态,就需要nextTick.

作用:将回调延迟到下次dom更新循环结束之后进行。如果想基于数据更新后的dom执行某些操作,可以把回调函数传入nextTick

vue中key的作用

  1. key的存在是为了更高效的更新虚拟DOM
  2. vue在比较新旧vnode时,会比较Key以及标签类型等信息是不是一致的,如果一致的话会去进行更新操作。如果不定义Key,那key就一直是undefined,两个key永远相等,vue就会认为两个节点是同一个,会进行不必要的更新操作,影响性能。
  3. vue中在使用相同标签的元素进行过渡切换时,会使用到key属性,没有key的话只会替换内部属性而不会触发过渡效果。
虚拟DOM(vnode)

虚拟dom就是用js对象来描述真实的dom节点。 diff算法去比较新旧vnode的差异,将变化的地方更新到真实的dom上。

  • diff是什么时刻触发的? 数据发生变化的时候,会触发setter通知,通知的方式是把watcher添加到异步更新队列,在每次事件循环队列结束时,清空事件队列,在这个过程中所有的watcher都会执行他们的更新函数,更新函数在执行的时候其实调用了我们的组件渲染函数和组件更新函数,这时会重新渲染最新的虚拟dom,然后执行更新函数,比较新旧虚拟dom。
  • dom-diff其实就是创建、删除、更新节点
  1. vnode创建 有注释节点、文本节点、元素节点能被创建、插入到dom节点中 判断vnode是否有tag标签,如果有,创建标签节点;如果没有,判断vnode是否有iscommend属性,如果有,创建注释node;如果没有则创建文本node
  2. vnode删除 如果新的vnode中没有而旧的vnode中有,则在旧的vnode中删除即可。
  3. vnode更新 更新节点有三种情况
  • vnode为静态节点:无需比较直接跳过
  • vnode为文本节点 如果vnode为文本节点,此时如果旧节点也是文本节点,只需比较文本的差异进行更新。如果旧节点不是文本节点,则直接替换成vnode
  • vnode为元素节点 (1)vnode含有子节点 a. 如果新的vnode中包含子节点,先检查旧的vnode中是否包含子节点,如果包含,则递归更新旧节点。如果旧节点不包含子节点,则直接创建一分新节点中的子节点插入到旧节点。如果旧节点中是文本节点,则把文本节点清空再把创建的子节点插入到旧节点。 (2)vnode不含子节点 b. 如果新的vnode中没有子节点,此时如果旧vnode中含有子节点,直接清空子节点。
  • vue3相对于vue2diff算法的优化点:
  1. 事件缓存
  2. 添加静态标记,vue2是全量diff,vue3是静态标记+部分diff
  3. 静态提升。后续直接复用静态节点
  4. vue2在updateChildren中对比变更,vue3是在patchKeyedChildren函数中,基于最长递增子序列去移动/添加/删除节点。

响应式原理

  1. vue2是通过数据劫持+发布订阅者模式去实现响应式的,是对data中的每个属性进行了递归遍历,为每个属性设置getter和setter。而vue3是对一个对象进行监听,只要对象发生变化就被监听到,这就完全可以代理所有属性。
  2. vue2中需要递归遍历对象中的所有属性, 如果一个对象中属性特别多或者是嵌套很深的话,会非常消耗性能。但vue3是在getter中递归,也就是只有真正被访问到的属性才会变成响应式,减少了性能消耗。
  • vue2响应式原理 对象: data通过observer把每个属性转换成了getter和setter的形式去追踪变化,当watcher实例读取属性时,会触发getter,从而被添加到依赖收集列表dep中,当属性发生变化的时候会触发setter,通知dep列表中所有watcher执行更新函数,从而更新视图。 vue2不能侦测到对象属性的新增和删除

数组:也是在getter中进行依赖收集,不同的是,数组的响应性是用方法拦截器让数组变得可观测。 vue2直接通过索引修改数组元素或者直接修改数组长度,这是不能被侦测到的。

  • vue3响应式原理 主要是在proxy的第二个参数handler中,track函数用来收集依赖,trigger函数用来触发更新。

vue2和vue3的区别

  1. 生命周期: vue3的生命周期都在前面加上了‘on’,并且setUp是围绕beforeCreated和created生命周期钩子运行的,所以不需要显示的定义

  2. 根节点: vue2只支持单根节点组件,vue3支持多根节点组件

  3. 组合式api: vue2是选项式api,一个逻辑会散乱在文件的不同位置,比如props|data|computed等,导致代码可读性变差,而vue组合式api将这些内容都写到一起,增强了代码的可读性、内举性

  4. diff算法优化

    • diff算法优化:在会发生变化的地方添加一个flag标记,下次发生变化的时候直接找该地方比较。
    • 静态提升:对于不会参与更新的元素会做静态提升,只会被创建一次,后续直接复用
    • 事件监听缓存:开启了事件监听缓存后,就没有了静态标记,在下次diff算法的时候可以直接使用
  5. 响应式系统优化

    • vue3使用proxy重写了响应式系统,实现对整个对象的监听,不需要深度遍历
  6. 源码体积

    • 移除了不常用的api
    • 打包的时候使用了treeShanking,比如ref、reactive、computed这些api只有在用到才会被打包,没有用到就会被摇掉,打包的整体体积变小

权限控制

  1. 登录权限

用户登录后后端会返回token,前端把token保存在localeStorage中,通过axios进行接口请求拦截,发送请求时把token携带在header中。并且利用axios做响应拦截,如果token验证没有通过,则跳转到登录页面

  1. 路由权限

初始化的时候先挂载不需要权限控制的路由,比如登录登录页、404页面,如果用户通过url强制访问,那就会进入404页面 首先请求当前登录用户所拥有的路由权限,然后筛选有权限的路由,在全局路由守卫中用addRoutes动态的添加路由

  1. 菜单权限

菜单由后端返回,前后端利用路由的name做关联,后端接口返回的name应该是唯一的,如果name在接口返回的菜单信息中不存在,就代表没有权限,不会出现在菜单中

  1. 控件权限

利用自定义指令控制按钮的权限,把当前用户的按钮权限信息保存在localeStorage中,判断自定义指令传的值或者当前路由的meta中的按钮权限信息是不是在当前用户的按钮权限范围内,在则显示,否则隐藏

自定义指令

自定义指令可以全局注册也可以局部注册。全局注册:vue.directive($name, )

应用场景:

  1. 按钮的权限控制
  2. 防止表单重复提交

keep-alive

keep-alive是vue的内置组件,能够在多个组件进行切换时缓存被移除的组件实例。

  • include 字符串或者正则,匹配到的组件就会被缓存
  • exclude 字符串或者正则,匹配到的组件不会被缓存
  • max 最大缓存的数量,如果缓存的组件数量超过这个值,那最久的没有被访问过的组件实例就会被销毁\

新增的生命周期:activited, deactivited

activited: 组件被首次挂载或者当一个组件实例作为缓存树的一部分插入到 DOM 中时调用 deactivited: 组件被卸载或者组件被移除但被缓存起来仍然作为组件树的一部分时调用。

作用域

作用域也叫执行上下文,规定了执行代码对变量的访问权限。

全局作用域和函数作用域
  1. 全局作用域
  • 最外层函数和最外层函数外面定义的变量拥有全局作用域。
  • 所有未定义直接赋值的变量自动声明为全局作用域。
  • 所有window对象的属性拥有全局作用域。
  1. 函数作用域
  • 声明在函数内部的变量拥有函数作用域
块级作用域

let,const声明的变量具有块级作用域,块级作用域可以在函数中创建也可以在代码块中创建。

var,let,const的区别

  1. var存在变量提升,声明的变量会挂载在windows上,可以重复声明。
  2. let,const具有块级作用域、不存在变量提升、暂时性死区、不能重复声明

注意:const声明之后必须马上赋值,否则会报错;const声明的基本类型不能更改,声明的复杂类型不能更改内存地址,可以更改内部属性。

在当前作用域中查找变量时,没有找到就会从父级作用域查找,还没有找到就依次向上查找,查找的最后一个对象是全局对象。查找的这条链就叫作用域链。

闭包

一个函数能够访问另一个函数内部的变量的时候,就形成闭包

为什么会内存泄漏?

因为当外层函数执行完毕后,外层函数执行环境的作用域链会销毁,但此时内部函数还在引用外层函数的活动对象,这个活动对象会留在内存中。 解决办法:执行外层函数的时候会分配一块堆内存,把堆内存中的值置为null就行了

内存泄漏的场景:
  • 意外的全局变量 使用了未声明的变量,变量就会变成全局变量,那这个变量就会一直保存在内存中无法被回收
  • 定时器:如果定时任务不被使用了,但是没有清除,那这个定时器所引用的外部变量就会一直保持在内存中无法被回收。解决办法:不使用定时器时及时清除定时任务
  • 闭包:外部函数执行时会分配一块堆内存,外部函数执行完之后如果没有对堆内存置空,那内部函数就会仍然引用外部函数的变量,导致变量无法被回收。解决办法:外部函数执行完之后将对应的堆内存置空
  • dom的引用:在引用一个dom元素之后,删除这个dom元素,此时dom元素已经不存在了,但是这个元素的引用还存在,所以会造成内存泄漏。解决办法:应该在移除之后将引用清空。 使用场景

1.回调函数

bash
复制代码
function add(num1,num2,callback){
let sum = num1+num2;
callback(sum)
}

add(1,2,function(sum){
    console.log(sum)
})

2.防抖节流

防抖:事件触发n秒后再执行回调,如果n秒内被再次触发,则重新及计时,比如浏览器上的模糊搜索

javascript
复制代码
function debounce(fn, delay) {
    let timer = null;
    return function() {
        if(timer !== null) {
            clearTimeout(timer) //如果计时任务已经存在,并且又触发了相同的事件,则取消当前计时,重新计时
        }
        timer = setTimeout(
          () => fn.call(this), delay)
    }
}

节流:事件连续触发,但n秒内只执行一次回调,比如:scroll,resize事件

ini
复制代码
function throttle(fn, delay) {
   let flag = true;
   return function() {
       if(flag) {
           setTimeout(() => {
           fn.call(this);
           flag = true;
           }, delay)
       }
       flag = false 
   } 
}

apply的传参只能是一个数组;call的传参可以传多个;bind用来创建一个函数,也可以传参

  1. 柯里化函数
javascript
复制代码
function uri(protocol) {
    return function (hostname, pathname) {
        return `${prootocol}${hostname}${pathname}`
    }
}
uri('http://')('xxx', 'yyy')

Promise

什么是promise?

promise是一种异步编程解决方案,它是一个对象,可以从中获取异步操作的消息。用.then的链式调用解决了多层异步操作时的回调地狱问题

promise有三种状态:pending,resolved,rejected,promise必然处于其中之一,状态一旦发生改变就不会再变,状态的改变是通过resolve()和reject()方法来实现的。

promise实例方法有catch,then,finally

Promise的静态方法有all,race,resolve,reject等,all比较常用,通常用来处理没有先后顺序的请求。

  • .all中只要有一个promise返回失败就会失败
  • .race含有竞速的意思,将多个Promise放在一个数组中,数组中第一个得到结果的promise,不管是" 完成
  • (resolved)"还是" 失败(resolved)" , 那么这个 .race 方法就会返回这个结果。
  • .resolve 返回一个resolved状态的Promise实例
  • .reject返回一个rejected状态的Promise实例

解决了什么问题?

  • 回调地狱:比如ajax请求有前后依赖关系时,那就需要嵌套ajax请求,多了的话就会嵌套很多层,也就是回调地狱promise的作用就是解决ajax的回调地狱问题,将嵌套请求变成链式调用
  • 可支持并发请求,获取并发请求的结果
async/await

async/await其实是一个语法糖,返回的是一个promise,他能实现的promise都能实现,相当于promise的一个优化。

  • async代码更清晰易于阅读,Promise的链式调用长了就会造成阅读负担。
  • async几乎是同步的写法,非常优雅,promise传递中间值非常麻烦
  • async调试更友好。promise中,由于没有代码块,你不能在一个返回表达式的箭头函数中设置断点。如果你在.then代码块中设置断点,调试器不能进入下一个.then。

js中有哪些数据类型?有什么区别

基本数据类型: undefined、null、string、boolean、number、symbol

symbol代表独一无二的数据类型,主要是为了解决可能出现的全局变量冲突的问题

原始数据类型存储在栈中,大小固定,占用空间小

复杂数据类型: 对象、数组、函数

复杂数据类型存储在堆中,引用类型的变量其实是一个指针,指向的是堆内存中的地址。

typeof和instanceof的区别

typeof判断数组|对象|null结果都是object.判断其余基本数据类型和function都能正确判断

instanceof能够正确判断引用数据类型。instanceof的运行机制是在数据的原型链中能否找到该类型的原型。

Object处理了一些特殊情况,比如

== 和 === Object.is的区别

===:严格相等,比较类型和值 ==:抽象相等,先对左右两边的值进行类型转换再比较值 Object.is 一般和===的判断结果相同,它处理了一些特殊情况,比如:

vbnet
复制代码
Object.is(-0,+0) //false
Object.is(NaN, NaN) //true
ini
复制代码
undefined == null //true
undefined === null //false

浅拷贝和深拷贝

引用类型的变量其实是一个指针,这个指针指向的是堆内存中的地址。

浅拷贝新旧对象还是共享同一块内存,但深拷贝是为新对象开辟了一个新的内存空间。

image.png

  1. 直接赋值 引用类型直接复制其实是不同的指针指向了相同的引用对象,所以会相互影响。
  2. 浅拷贝 浅拷贝是创建一个新的对象,浅拷贝只是复制了第一层对象的属性,因此如果对象内的属性是引用类型,修改是会相互影响的。但第一层数据类型为基本类型时,浅拷贝后不会相互影响。

浅拷贝的方式:

javascript
复制代码
数组:Array.concat()  Array.slice()
对象:Object.assign({}, obj)
复制代码

3. 深拷贝 递归拷贝对象中的每一个属性,所以深拷贝后两个对象之间互不影响。

深拷贝的方式:

javascript
复制代码
JSON.parse(JSON.stringify(obj)) //缺点:对象中包括日期或者正则表达式时,会跟源数据不一致
scss
复制代码
function deepClone(source) {
    if(source ===null) return source;
    if(source instanceof Date) return new Date();
    if(source instanceof RegExp) return RegExp;
    if(typeof source !== 'object') return source
    let obj = Array.isArray(source) ? [] : {};
    for(let i in source) {
        if(source.hasOwnProperty(i)) {
            obj[i] = deepClone(source[i])
        }
    }
    return obj;
}
复制代码
lodash中的cloneDeep

原型和原型链

什么是原型?

原型分为隐式原型和显示原型。每个引用类型的数据都有一个隐式原型__proto__,这个值指向它的构造函数的显式原型prototype, 这两个值完全相等。

什么是原型链?

image.png 当查找对象中的属性或者方法时,首先会从自身查找,如果没有查找到,就会去它的原型上去找,如果再找不到,就会去原型的原型上去找,直到null为止。查找的这条链就叫原型链。

原型链是一个过程,原型则是这个过程中的单位,贯穿整个原型链。

js运行机制

js是单线程的,也就是所有的任务都需要排队,如果前面的任务没有执行完,那么后面的任务就需要一直等,这样用户体验很差,所以就有了异步的概念。

同步任务:在主线程排队的任务

异步任务:不进入主线程,会被放进任务队列。分为宏任务和微任务

go
复制代码
```
宏任务:script,setTimeout,setInterval
复制代码
```
```
微任务:promise.then,nextTick,async await   (需要耗时的一些任务)
复制代码
```

同步任务依次执行完之后,会从任务队列中拿到宏任务来执行,宏任务执行完后会执行该任务中的微任务,以此循环。即事件循环。 计时器为什么不准?计时器不是到点就执行,而是到点放到任务队列中,轮到它了再去执行

事件冒泡和捕获

事件冒泡:事件在最内层元素发生,并且向上传播。

事件捕获:事件在最外层元素发生,直到最具体的元素。

bash
复制代码
element.addEventListener(event, fn, useCapture)   //useCapature为true时表示在捕获阶段调用处理函数,为false表示在冒泡阶段调用处理函数。

场景: 在点击li标签时,需要执行某些操作,常规做法是去遍历li标签绑定事件,那当li标签特别多的时候,很消耗性能。如果利用事件冒泡,那此时只需要在ul元素上去监听点击事件,事件在li元素上发生,会传播到ul元素,此时会触发ul的点击事件,再利用target去判断事件的发生地,就可以去执行操作了

继承

  1. 原型链继承
javascript
复制代码
function A(name, age) {
    this.name = name;
    this.age = age;
}
A.prototype.getAge = (name) => name
function B(){}
B.prototype = new A()
复制代码

缺点:原型链上的修改都会变成通用的

  1. 构造函数继承
javascript
复制代码
function A(name, age) {
    this.name = name;
    this.age = age;
}
A.prototype.getAge = (name) => name
function B(name){
    A.call(this,name)
}
const b = new B('aa')
复制代码

缺点:不能继承原型链上的方法

  1. 组合继承(原型+构造函数)
javascript
复制代码
function A(name, age) {
    this.name = name;
    this.age = age;
}
A.prototype.getAge = (name) => name
function B(name){
    A.call(this,name)
}
B.prototype = new A('hh')
复制代码

缺点:初始化了两次

  1. 寄生组合继承
javascript
复制代码
function A(name, age) {
    this.name = name;
    this.age = age;
}
A.prototype.getAge = (name) => name
function B(name){
    A.call(this,name)
}
B.prototype = Object.create(A.prototype)

get和post请求的区别

类别get请求post请求备注
用途用来获取资源用来创建或者获取资源
参数位置get的参数会携带在url上Post的参数在请求体中
安全性get请求信息在url上,不安全post比较安全
参数大小url中参数最大为2048个字符Post的参数大小没有限制
数据包get请求只会产生一个tcp数据包post请求会产生两个tcp数据包1、对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);2、而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。
浏览器回退get请求在浏览器回退时不会重新发起请求post请求会

Proxy

proxy是ES6中新增的功能,可以自定义对象中的操作。Vue3中使用Proxy代替Object.defineProperty来实现数据响应式。

es6新特性

  1. let, const

  2. 数组

    • 扩展运算符
    • Array.from(),Array.of()
    • find(),findIndex()
    • fill(value, startIndex, endIndex)
    • includes()
    • flat()
    • entries(),keys(),values()
  3. 对象

    • Object.is(),Object.assign()
    • Object.keys(),values(),entries()
  4. promise

  5. 模块化(import,export)

数组方法

  1. map
  2. forEach
  3. push
  4. pop
  5. shift
  6. unshift
  7. filter
  8. reduce
  9. concat
  10. slice
  11. splice
  12. reverse
  13. sort
  14. some, every

字符串方法

trim,concat,slice,substring,includes,chartAt,split,match,toUpperCase,toLowerCase

token失效

用户登录成功后,会返回一个token和refresh_token 前端请求接口api --> 返回401错误 --> 前端判断是否有refresh_token -->如果有就用refresh_token请求新的token --> 后台成功返回一个新的token和refreshToken给我们 --> 更新vuex+本地存储持久化 --> 带上新的token请求数据 如果没有token就直接重定向到登录页

性能优化

  • 加载时的优化
  1. 减少http请求

dns -> tcp -> 发起请求 -> 服务器接收请求 -> 服务器处理请求并响应 -> 浏览器接收响应 2. 服务端渲染

服务端返回html文件,客户端只需要解析html文件即可

客户端渲染是获取html文件,根据需要下载js文件,运行文件,生成dom再渲染

  1. 静态资源使用CDN

多个位置部署服务器,让用户离服务器更近,缩短请求时间

  1. css写在头部,html在中间,js在底部

css,js会阻塞dom的构建,css会阻塞js的执行

  1. 字体图标代替图片图标

字体图标是矢量图不会失真,生成的文件特别小

  1. 利用缓存

expires,cache-control,在资源过期之前都可以读取缓存而不需要重新请求

  1. 图片优化

切图使用jpg格式,雪碧图

  1. webpack按需加载代码
  • 运行时的优化
  1. 修改样式时使用类改变样式,而不是直接写行内样式
  2. 事件委托(节省内存)
  3. 降低css选择器的复杂性
  4. 使用弹性布局
  5. 用transform和opicity实现动画,不会引起重排

状态码

200 成功 301 永久重定向 302 临时重定向;304 存在缓存; 401 身份验证通过;403 没有操作权限;400 资源不存在;404 请求错误;500 服务器错误;504 网关超时

跨域

协议、域名、端口有一个不一致时,会发生跨域

  • proxy代理,在vue.config.js中设置目标地址
  • jsonp实现(需要前端和后端相互配合)
js
复制代码
    script标签没有同源限制, 前端动态的创建script标签,src中传入要请求的地址,然后在请求的query参数 中传入callback参数,参数值为当前文件中的一个全局函数。服务器收到请求后,会返回一个数据,这个时候浏览器会去执行query参数中的全局函数,这时就可以拿到服务器的数据了。
    jsonp只能处理get请求
  • cors实现(服务端实现)
js
复制代码
客户端发起请求
服务器端设置相关的头信息:
access-control-allow-origin:
access-control-allow-headers
access-control-allow-methods

不需要前端携带cookie时,后端设置Access-Control-Allow-Origin: 设置为 * , 前端设置withCredentials为false。否则后端设置Access-Control-Allow-Origin: 设置为项目的根域名 , 前端设置withCredentials为true

  • http proxy (webpack-dev-server) (前端处理)
js
复制代码
devServer: 设置proxy代理,changeOrigin: true