发布于 

整理下近期面试被问到的面试题(前端 3-5 年经验)

js 中 new 操作符干了什么

  1. 创建一个空对象
  2. 让新对象继承构造函数的原型对象
  3. 调用构造函数,传入实参,并自动替换构造函数中的 thisnew 正在创建的新对象。构造函数中,通过强行赋值的方式为新对象添加规定的属性,并保存属性值
  4. 返回新对象的地址,保存到=左边的变量中

但是 eslint 已经有了不让 new 的规则,大部分新生 API 都采用 create 方式,比如 vue3

vue2 和 vue3 构建实例区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import plugins from './components/common/index'

// 注意必须在构建 Vue 实例之前就将需要的组件注册进去
Vue.use(plugins);

Vue.component('button-counter', {
data: () => ({
count: 0
}),
template: '<button @click="count++">Clicked {{ count }} times.</button>'
})

Vue.config.productionTip = false

new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')

在 vue2 中注册全局组件、挂载原型、全局插件必须在构建 Vue 实例之前,其实这样非常污染 Vue 实例

1
2
const app1 = new Vue({ el: '#app-1' })
const app2 = new Vue({ el: '#app-2' })

此时如果创建两个 Vue 实例,会导致每个实例都挂载了相同的插件、全局组件,因为插件的注册是在 new Vue 之前的,即挂载在 Vue 原型上

除了 component 还有以下全局 API 都会影响到 Vue 实例:

  • Vue.directive()
  • Vue.mixin()
  • Vue.use
  • Vue.config
  • Vue.prototype

其原因其实是因为 Vue2 版本是没有考虑到多个应用程序的,这使得创建 Vue 的副本非常困难。

构造函数的形式不利于隔离不同的 Vue 实例应用。调用构造函数的静态方法会对所有 Vue 实例应用生效

构造函数的形式不利于 Tree Shaking,无论我们使用什么功能,它们最终都会出现在生产代码中。主要原因是 Vue 实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到

1
2
3
4
import Vue from 'vue'
Vue.mixin()
Vue.use()
Vue.nextTick(() => {})

Vue3 引入 Tree Shaking 特性,将全局 API 进行分块。如果你不使用其某些功能,它们将不会包含在你的基础包中

1
2
3
import { nextTick, observable } from 'vue'

nextTick(() => {})

通过 Tree ShakingVue3 给我们带来的好处是:

  • 减少程序体积(更小)
  • 减少程序执行时间(更快)
  • 便于将来对程序架构进行优化(更友好)

其次 Vue3 采用 createApp 工厂函数来返回一个 Vue 实例,所有全局 API 修改都通过这个 实例 来挂载处理,这样,多个 createApp 创建的实例,它们之间互相不干扰

1
2
3
4
5
6
const { createApp } = Vue
const app = createApp({})

app.component('my-component', {
/* ... */
})

ts 中 interface 和 type 的区别

两者都可以描述对象或者函数

interface 侧重于描述数据结构,这个结构该有哪些类型的变量

type 侧重描述类型

type 重复定义会报错

interface 重复定义会自动合并

优先使用 interface

vue2 和 vue3 响应式的变化

vue2 和 vue3 都是在相同的生命周期(beforeCreate 之后、created 之前)完成数据的响应式。

vue2 的响应式原理是怎么样的?

vue2 的响应式对象是通过 Object.defineProperty 对每个属性进行监听,当对属性进行读取的时候,就会触发 getter,对属性进行设置的时候,就会触发 setter

由于 Object.defineProperty 无法监听对象的变化,所以 Vue2 中设置了一个 Observer 类来管理对象的响应式依赖,同时也会递归侦测对象中子数据的变化。

Observer 类的作用就是把一个对象全部转换成响应式对象,包括子属性数据,当对象新增或删除属性的时候负债通知对应的

Object.defineProperty 真的不能监听数组的变化吗?

其实 Object.defineProperty 是可以监听数组的变化的。

首先这种直接通过下标获取数组元素的场景就比较少,其次即便通过了 Object.defineProperty 对数组进行监听,但也监听不了 push、pop、shift 等对数组进行操作的方法,所以还是需要通过对数组原型上的那 7 个方法进行重写监听。所以为了性能考虑 vue2 直接弃用了使用 Object.defineProperty 对数组进行监听的方案。

性能问题到底指的是什么呢

  • 数组 和 普通对象 在使用场景下有区别,在项目中使用数组的目的大多是为了 遍历,即比较少会使用 array[index] = xxx 的形式,更多的是使用数组的 API 的方式
  • 数组长度是多变的,不可能像普通对象一样先在 data 选项中提前声明好所有元素,比如通过 array[index] = xxx 方式赋值时,一旦 index 的值超过了现有的最大索引值,那么当前的添加的新元素也不会具有响应式
  • 数组存储的元素比较多,不可能为每个数组元素都设置 getter / setter
  • 无法拦截数组原生方法如 push、pop、shift、unshift 等的调用,最终仍需 重写 / 增强 原生方法

vue2 对数组的监测是通过重写数组原型上的 7 个方法来实现 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'

看来 vue 能对数组进行监听的原因是,把数组的方法重写了。总结起来就是这几步:

  1. 先获取原生 Array 的原型方法,因为拦截后还是需要原生的方法帮我们实现数组的变化。
  2. Array 的原型方法使用 Object.defineProperty 做一些拦截操作。
  3. 把需要被拦截的 Array 类型的数据原型指向改造后原型。

vue3 的响应式原理是怎么样的?

详细内容看这篇文章:听说你很了解 Vue3 响应式?

  • vue3 中提供了 reactive()ref() 两个方法用来将 目标数据 变成 响应式数据,通过 Proxy 来实现 数据劫持(或代理)

  • 普通对象类型直接配合 Proxy 提供的捕获器实现响应式

  • 数组类型 也可以直接复用大部分和 普通对象类型 的捕获器,但其对应的查找方法和隐式修改 length 的方法仍然需要被 重写 / 增强

  • 原始值数据类型 主要通过 ref() 函数来进行响应式处理,不过内容不会对 原始值类型 使用 reactive()(或 Proxy) 函数来处理,而是在内部自定义 get value(){}set value(){} 的方式实现响应式,毕竟原始值类型的操作无非就是 读取设置,核心还是将 原始值类型 转变为了 普通对象类型

    • ref() 函数可实现原始值类型转换为 响应式数据,但 ref() 接收的值类型并没只限定为原始值类型,若接收到的是引用类型,还是会将其通过 reactive() 函数的方式转换为响应式数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// vue3的响应式原理
const person = {
name: "李四",
age: 20
}
const p = new Proxy(person, {
get(target, propName) { // 读取属性调用
// target:源对象 propName:属性名
return Reflect.get(target, propName) // Reflect ES6的语法
},
set(target, propName, value) { // 修改、追加属性调用
// target:源对象 propName:属性名 value:追加/修改的值
return Reflect.set(target, propName, value) // Reflect ES6的语法
},
deleteProperty(target, propName) { // 删除属性调用
return Reflect.deleteProperty(target, propName) // Reflect ES6的语法
},
})

proxy 内部使用 Reflect 静态方法来实现对数据的操作

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法,这些方法与 Proxy handler 提供的的方法是一一对应的,且 Reflect 不是一个函数对象,即不能进行实例化,其所有属性和方法都是静态的。

Proxy 为什么需要 Reflect 呢?

Proxyget(target, key, receiver)、set(target, key, newVal, receiver) 的捕获器中都能接到前面所列举的参数:

  • target 指的是 原始数据对象
  • key 指的是当前操作的 属性名
  • newVal 指的是当前操作接收到的 最新值
  • receiver 指向的是当前操作 正确的上下文

怎么理解 Proxy handler 中 receiver 指向的是当前操作正确上的下文呢?

正常情况下,receiver 指向的是 当前的代理对象

特殊情况下,receiver 指向的是 引发当前操作的对象

  • 通过 Object.setPrototypeOf() 方法将代理对象 proxy 设置为普通对象 obj 的原型
  • 通过 obj.name 访问其不存在的 name 属性,由于原型链的存在,最终会访问到 proxy.name 上,即触发 get 捕获器

Reflect 的方法中通常只需要传递 target、key、newVal 等,但为了能够处理上述提到的特殊情况,一般也需要传递 receiver 参数,因为 Reflect 方法中传递的 receiver 参数代表执行原始操作时的 this 指向,比如:Reflect.get(target, key , receiver)Reflect.set(target, key, newVal, receiver)

总结:Reflect 是为了在执行对应的拦截操作的方法时能 传递正确的 this 上下文

说说 v3 中 Tree Shaking 特性?举例说明一下?

Tree Shaking 是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 Dead code elimination

简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码

Vue2 中,无论我们使用什么功能,它们最终都会出现在生产代码中。主要原因是 Vue 实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到

1
2
3
import Vue from 'vue'

Vue.nextTick(() => {})

Vue3 源码引入 Tree Shaking 特性,将全局 API 进行分块。如果您不使用其某些功能,它们将不会包含在您的基础包中

1
2
3
import { nextTick } from 'vue'

nextTick(() => {})

v2 和 v3生命周期的变化

vue2 vue3
beforeCreate setup()
created setup()
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
activeted onActiveted
deactiveted onDeactiveted
beforeDestory onBeforeUnmount
destoryed onUnmounted

v-for 和 v-if

vue2 中 v-for 优先级高于 v-if,虽然 vue2 规范中不建议 v-for 和 v-if 同写一行,因为在 循环中+判断 这样会带来性能问题。

vue3 中 v-if 优先级高于 v-for,因为 vue3 觉得 vue2 既然不推荐 v-for 和 v-if 同行,那设置优先级本身没有什么意义。

nextTick 的作用

nextTick 这个方法作用是当数据被修改后使用这个方法,回调函数获取更新后的 DOM 再渲染出来

说明:

nextTick 是一个异步微任务,等待当前函数的 DOM 渲染结束后执行
nextTick 类似于一个非常高级的定时器 自动追踪 DOM 更新 更新好了就触发
应用场景

DOM 更新是异步的, vue 响应式的特征是修改数据后页面会自动更新,而更新 DOM 这个操作是异步的 ,这个时候使用 nextTick 回调函数会在下一次 DOM 更新完毕后执行

key

  1. 当使用 <template> 进行v-for时,vue3需要把 key 放在 <template>,而不是把key放在子元素中。(vue2 是把 key 放在子元素)。
  2. 当使用 v-if、v-else-if、v-else 不再需要使用 key,因为 vue3 会自动给予每个分支一个唯一的 key

watchEffect 与 watch 有什么不同

  1. watchEffect 不需要指定监听的属性,他会自动的收集依赖, 只要我们回调中引用到了响应式的属性, 那么当这些属性变更的时候,这个回调都会执行
  2. watch 只能监听指定的属性
  3. watch 是惰性的,如需组件初始化就执行请携带 immediate: true 参数
  4. watch 可以获取到新值与旧值,而 watchEffect 不行
  5. watchEffect 在组件初始化的时候就会执行一次用以收集依赖(与computed同理),后续收集的依赖发生变化,这个回调才会再次执行

vue3 setup

vue3 组件入口为 setup(){} 函数作为入口, 默认只执行一次;执行顺序在 beforeCreate 之后 created 之前

Composition API 和 Option API 的区别

  1. Composition API 函数式开发,很大程度的提高组件、业务逻辑的复用性;高度解耦;提升代码质量、开发效率;减少代码体积
  2. Option API 在单文件组件中过长会出现一个逻辑关注点分散在组件各处,形成代码碎片,维护时需要反复横跳

回答范例:

vue3 首推 Composition API,但是这会让我们在代码组织上多花点心思,因此在选择上,如果我们项目属于中低复杂度的场景,Option API 仍然是一个好选择。对于那些大型,高扩展,强维护的项目上, Composition API 会获得更大的收益。

刷新后 vuex 状态丢失怎么办

localStorage 持久化存储,或者第三方插件存储

vuex 有什么缺点

模块化这一块做的过于复杂,用的时候容易出错。比如访问 store 时要带上模块 key,内嵌模块的话会很长,不得不配合 mapState,对 ts 支持不友好,使用模块时没有代码提示,pinia 出现之后使用体验好了很多,vue3 + pinia 会是更好的组合

什么场景下使用嵌套路由

  1. 平时开发中,应用的有些界面是由多层组件组合而来的,在这种情况下,url 各部分 通常对应某个嵌套的组件,vue-router 中可以使用嵌套路由表示这种关系
  2. 表现形式是在两个路由间切换时,他们有公用的视图内容。此时通常提取一个父组件,内部放上 <router-view>,从而形成物理上的嵌套,和逻辑上的嵌套对应起来。定义嵌套路由时使用 children 属性组织嵌套关系
  3. 原理上是在 <router-view> 组件内部判断其所处的嵌套层级的深度,将这个深度作为匹配组件数组 matched 的索引,获取对应的渲染组件并渲染之

vue-router history 和 hash 模式有什么区别

主要区别是在他们的展现形式,url 的显示形式和部署上

hash 模式在地址栏的时候是已哈希的形式:#/xxx,这种方式使用和部署都比较简单

history 模式 url 看起来更优雅美观,xxx/xx,但是应用在部署时需要特殊配置,web服务器需要做回退处理,否则会出现刷新页面404的问题

在实现上不管哪种模式都是监听 popstate 事件触发路由跳转处理,url 显示不同只是显示效果上的差异

$route 和 $router 的区别

routerVueRouter 的实例,相当于一个全局的路由器对象,里面含有很多属性和子对象,例如 history 对象。经常用的跳转链接就可以用 this.$router.pushrouter-link 跳转一样。

route 相当于当前正在跳转的路由对象。可以从里面获取 name,path,params,query 等属性

router 路径传值

query 是显式传值(直接显式在 http://localhost:8080/about?a=1

params 是隐式传值

样式穿透

以下三种方式在 Vue3 中均已弃用,详见 Vue3 组件样式变化。

  • >>>:适用于css、stylus
  • /deep/:适用于node-sass、less
  • ::v-deep:适用于dart-sass、node-sass、less、stylus

注:截止 2022 年 5 月,以上 3 种旧的深度选择器任能在 vue3 项目中使用,但会有警告提示信息;

vue3 目前最新的样式穿透是 ::v-deep() 简写 :deep():深度选择器(样式穿透);

说一说 scoped 样式隔离

Vue 在创建组件的时候,会给组件生成唯一的 id 值,当 style 标签给 scoped 属性时,会给组件的 html 节点都加上这个 id 值标识,如 data-v4d5aa038,然后样式表会根据这 id 值标识去匹配样式,从而实现样式隔离

什么是 MVVM?

model-view-viewModel(MVVM) 是一个软件架构设计模式,能够实现前端开发和后端业务逻辑的分离,其中

model 指数据模型,负责后端业务逻辑处理
view 指视图层,负责前端整个用户界面的实现
viewModel 则负责 view 层和 model 层的交互

什么是 commonJS / AMD / CMD / ES6

什么是模块化:

可以简单的理解为将原来繁重复杂的整个 js 文件按照功能 或者按模块拆成一个个单独的 js 文件,然后将每一个 js 文件中的某些方法抛出去,给别的 js 文件引用和依赖

  • node.js 采取 commonJS 规范,因为是服务器编程,模块文件一般都已经存在本地硬盘,加载比较快,采用同步加载模块
  • 浏览器端一般采用 AMD 规范,浏览器环境要从服务器端加载模块,这时就必须采用异步模式,出的早,可以指定回调函数
  • CMD 规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行(延迟加载)。CMD 规范整合了 commonJSAMD 规范的特点

commonJS 模块时运行时加载,它输出一个值的拷贝(模块内改变不会影响输出的这个值)
se6 模块时编译时输出接口,是输出一个值得引用(引用会改变原值)

AMD

AMD(Asynchronous Module Definition):异步模块定义。采用异步方式加载模块,模块的加载不影响后续语句的执行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
requireJS 是一个遵守 AMD 规范工具库,用于客户端的模块管理。requireJS 的基本思想是,通过 define 方法,将代码定义为模块;通过 require 方法,实现代码的模块加载。

CMD

CMD(Common Module Definition):通用模块定义。用于浏览器端,是除 AMD 以外的另一种模块组织规范。结合了 AMD 与 CommonJs 的特点。也是异步加载模块。
与 AMD 不同的是:AMD 推崇的是依赖前置,而 CMD 是依赖就近,延迟执行。

CommonJS

Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。
CommonJs 有 4 个毕竟重要的变量:modulerequireexportsglobal

ES6

ES modules(ESM)是 JavaScript 官方的标准化模块系统。ES6 模块设计的思想是尽量的静态化,使得编译时就能知道模块的依赖关系,以及输入和输出的变量。有两个主要的命令:export 和 import。export 用于对外暴露接口,import 用于引入其他模块。

ES6 模块的特点:
  • 严格模式:ES6 的模块自动采用严格模式
  • import read-only 特性: import 的属性是只读的,不能赋值,类似于 const 的特性
  • export / import 提升: import / export 必须位于 模块顶级,不能位于作用域内;其次对于模块内的 import / export 会提升到模块顶部,这是在编译阶段完成的
  • 兼容在 node 环境下运行
  • ES modules 输出的是 值的引用,输出接口动态绑定,而 CommonJS 输出的是值的拷贝

总结

  • AMD:异步加载模块,允许指定回调函数。AMD 规范是依赖前置的。一般浏览器端会采用 AMD 规范。但是开发成本高,代码阅读和书写比较困难。
  • CMD:异步加载模块。CMD 规范是依赖就近,延迟加载。一般也是用于浏览器端。
  • CommonJs:同步加载模块,一般用于服务器端。对外暴露的接口是值的拷贝
  • ES6:实现简单。对外暴露的接口是值的引用。可以用于浏览器端和服务端。

什么是回调函数

将一个函数作为参数传递给另一个函数,并在函数体内部调用它。所以,被传递给另一个函数作为参数的函数叫作回调函数。比如:setTimeout

1
2
3
4
5
const message = function() {
console.log("This message is shown after 3 seconds");
}

setTimeout(message, 3000);

什么是类

类是 es6 新增的,是 es5 构造函数的语法糖,在 es5 时期,生成实例是通过构造函数。
es6 中使用 class 关键字声明一个类,之后以这个类来实例化对象。
类抽象了对象的公共部分,它泛指某一大类(class)对象特指某一个,通过类实例化一个具体的对象

es5 和 es6 构造实例的区别

接下来就进入正题了,揭开 es6 中 class 的神秘面纱。首先为什么会有 class 的概念,在 es5 时期,生成实例是通过构造函数,但是如果要添加方法的话,就必须在原型上去添加,这样构造函数 new 出来的实例才可以用这个方法。就比如这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Person为构造函数
function Person (name, age) {
this.name = name,
this.age = age
}

// 在构造函数的原型上添加方法
Person.prototype.getName = function () {
return this.name
}

// 构造一个实例
var huhaha = new Person('huhaha', 21)
// 在实例上调用该方法
var myName = huhaha.getName()

console.log(myName) // huhaha

而在 es6 中使用 class 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 类中的this指向创建的实例
class Person {
constructor (name, age) {
this.name = name
this.age = age
}

getName () {
return this.name
}
}

// 构造一个实例
let huhaha = new Person('huhaha', 21)
// 在实例上调用该方法
const myName = huhaha.getName()

console.log(myName) // huhaha

类,对象,面向对象总结

类抽象了对象的公共部分,它泛指某一大类(class)
对象特指某一个,通过类实例化一个具体的对象

面向对象的思维特点:

抽取(抽象)对象共用的属性和行为组织(封装)成一个类(模板)
对类进行实例化、 获取类的对象
实例:实际的例子、对象
实例化:通过类的构造函数,来创建对象、实例

简述原型与原型链,原型链的作用有哪些?

  • 构造函数:用来初始化新创建的对象的函数是构造函数。在例子中,Foo() 函数是构造函数
  • 原型对象:构造函数有一个 prototype 属性,指向实例对象的原型对象。通过同一个构造函数实例化的多个对象具有相同的原型对象。经常使用原型对象来实现继承。
  • 实例对象:通过构造函数的 new 操作创建的对象是实例对象。可以用一个构造函数,构造多个实例对象

每一个实例对象都有一个 (原型对象,es6 里叫 [[Prototype]]: Object

prototype 中有一个隐式 __proto__ 属性(隐式原型),默认值是构造函数的 prototype

__proto__ 的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的 __proto__ 属性所指向的那个对象(父对象)里找,一直找直到 __proto__ 属性的终点 null,再往上找就相当于在 null 上取值就会报错。
通过 __proto__ 属性将对象连接起来的这条链路即我们所谓的原型链。

  • 一切对象都是继承自 Object 对象,Object 对象直接继承根源对象 null
  • 一切的函数对象(包括 Object 对象),都是继承自 Function 对象
  • Object 对象直接继承自 Function 对象
  • Function 对象的 __proto__ 会指向自己的原型对象,最终还是继承自 Object 对象

img

var、const、let的区别

var 存在变量提升,js 在预编译的时候会自动将所有代码里面的,var 关键字生命的语句提升到当前作用域的顶端

1
2
3
4
5
6
7
8
9
function person(status) {
if (status) {
var value = "蛙人"
} else {
console.log(value) // undefined
}
console.log(value) // undefined
}
person(false)
1
2
3
4
5
6
7
8
9
10
11
// 上述代码会变成这样
function person(status) {
var value;
if (status) {
value = "蛙人"
} else {
console.log(value) // undefined
}
console.log(value) // undefined
}
person(false)

es6 带来了块级声明则是 const let

  • 只在当前函数声明下有效
  • 在代码块和 {} 括号之间有效

letvar 一样都是声明变量,但是 let 没有变量提升,let 声明的变量只在当前块级有效

1
2
3
4
5
6
7
8
9
function person(status) {
if (status) {
let value = "蛙人"
} else {
console.log(value) // 报错
}
console.log(value) // 报错
}
person(false)

let 不允许重复声明,会报错

const 声明指的是常量,const 定义必须初始化值否则会报错

1
2
const value = "hhhh"
const age; // 报错 常量未初始化

const 一旦定义了值或对象,那么它的内存地址则不能修改,比如定义一个对象,你可以修改对象里面的属性,但是无法修改对象

1
2
3
4
5
6
const person = {
name: "alice",
age: 23
}
person.age = 18 // 没问题
person = {} // 报错 不能修改对象指针

在 const let 定义变量之前调用该变量是会报错还是报 undefined

答案是报错,如下:
Uncaught ReferenceError: Cannot access 'b' before initialization
意思就是在初始化之前无法访问变量 b

1
2
3
4
5
function a () {
console.log(b)
const b = '123'
}
console.log(a()) // Uncaught ReferenceError: Cannot access 'b' before initialization

var let const 最大的区别

var 在全局作用域声明的变量有一种行为会挂载在 window 对象上,这种行为有可能会覆盖 window 的某个属性,而 let、const 则不会有这个行为

1
2
3
4
5
6
var value1 = "张三"
let value2 = "李四"
const value3 = "王五"
console.log(window.value1) // 张三
console.log(window.value2) // undefined
console.log(window.value3) // undefined

使用 echarts 有没有遇到过图表模糊的情况

当时我回答的没有,没有想到面试官问的是 echarts 图表渲染的两种方式, SVGCanvas

一般来说,Canvas 更适合绘制图形元素数量较多(这一般是由数据量大导致)的图表(如热力图、地理坐标系或平行坐标系上的大规模线图或散点图等),也利于实现某些视觉 特效。但是,在不少场景中,SVG 具有重要的优势:它的内存占用更低(这对移动端尤其重要)、并且用户使用浏览器内置的缩放功能时不会模糊
选择哪种渲染器,我们可以根据软硬件环境、数据量、功能需求综合考虑。

  • 在软硬件环境较好,数据量不大的场景下,两种渲染器都可以适用,并不需要太多纠结。
  • 在环境较差,出现性能问题需要优化的场景下,可以通过试验来确定使用哪种渲染器。比如有这些经验:
    • 在需要创建很多 ECharts 实例且浏览器易崩溃的情况下(可能是因为 Canvas 数量多导致内存占用超出手机承受能力),可以使用 SVG 渲染器来进行改善。大略的说,如果图表运行在低端安卓机,或者我们在使用一些特定图表如 水球图 等,SVG 渲染器可能效果更好。
  • 数据量较大(经验判断 > 1k)、较多交互时,建议选择 Canvas 渲染器。

如何使用渲染器
如果是用如下的方式完整引入 echarts,代码中已经包含了 SVG 渲染器和 Canvas 渲染器

1
import * as echarts from 'echarts';

如果你是按照 在项目中引入 Apache ECharts 一文中的介绍使用按需引入,则需要手动引入需要的渲染器

1
2
3
4
import * as echarts from 'echarts/core';
// 可以根据需要选用只用到的渲染器
import { SVGRenderer, CanvasRenderer } from 'echarts/renderers';
echarts.use([SVGRenderer, CanvasRenderer]);

然后,我们就可以在代码中,初始化图表实例时,传入参数 选择渲染器类型:

1
2
3
4
5
6
7
// 使用 Canvas 渲染器(默认)
var chart = echarts.init(containerDom, null, { renderer: 'canvas' });
// 等价于:
var chart = echarts.init(containerDom);

// 使用 SVG 渲染器
var chart = echarts.init(containerDom, null, { renderer: 'svg' });

讲一下闭包

闭包让你可以在一个内层函数中访问到其外层函数的作用域

一个函数和词法环境的引用捆绑在一起,这样的组合就是闭包(closure)。

一般就是一个 func A,return 其内部的 func B,被 return 出去的 func B 能够在外部访问 func A 内部的变量,这时候就形成了一个 func B变量背包func A 执行结束后这个变量背包也不会被销毁,并且这个变量背包在 func A 外部只能通过 func B 访问。

1
2
3
4
5
6
7
8
9
10
function A(){
let a1 = 1;
return function B(){
return a1;
}
}
// 外部如何访问到 func B,我们只需要将 func B 作为 func A 的返回值返回,
// 这样我们不就能在 func A 外部访问到 func A 内部的变量了
var result = A();
result(); // 1

闭包形成的原理: 作用域链,当前作用域可以访问上级作用域中的变量。
闭包解决的问题: 能够让函数作用域中的变量在函数执行结束之后不被销毁,同时也能在函数外部可以访问函数内部的局部变量。
闭包带来的问题: 由于垃圾回收器不会将闭包中变量销毁,于是就造成了内存泄露,内存泄露积累多了就容易导致内存溢出。
闭包的应用场景: 能够模仿块级作用域,能够实现柯里化,在构造函数中定义特权方法、Vue 中数据响应式 Observer 中使用闭包等。

函数作为参数:柯里化函数
柯里化的目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 假设我们有一个求长方形面积的函数
function getArea(width, height) {
return width * height
}
// 如果我们碰到的长方形的宽老是10
const area1 = getArea(10, 20)
const area2 = getArea(10, 30)
const area3 = getArea(10, 40)

// 我们可以使用闭包柯里化这个计算面积的函数
function getArea(width) {
return height => {
return width * height
}
}

const getTenWidthArea = getArea(10)
// 之后碰到宽度为10的长方形就可以这样计算面积
const area1 = getTenWidthArea(20)

// 而且如果遇到宽度偶尔变化也可以轻松复用
const getTwentyWidthArea = getArea(20)

在 script 标签上使用 defer 和 async 的区别

script 标签存在两个属性,deferasync,因此 script 标签的使用分为三种情况:

1
<script src="example.js"></script>

没有 deferasync 属性,浏览器会立即加载并执行相应的脚本。也就是说在渲染 script 标签之后的文档之前,不等待后续加载的文档元素,读到就开始加载和执行,此举会阻塞后续文档的加载;

1
<script async src="example.js"></script>

有了 async 属性,表示后续文档的加载和渲染与 js 脚本的加载和执行是并行进行的,即异步执行;

1
<script defer src="example.js"></script>

有了 defer 属性,加载后续文档的过程和 js 脚本的加载(此时仅加载不执行)是并行进行的(异步),js 脚本的执行需要等到文档所有元素解析完成之后,DOMContentLoaded 事件触发执行之前。

总结

async defer 都是异步的,但是 async 边异步加载边执行,defer 只是异步加载,延迟执行(等文档元素解析完成后再执行)

图片懒加载原理

原理

一张图片就是一个标签,浏览器是否发起请求图片是根据的 src 属性,所以实现懒加载的关键就是,在图片没有进入可视区域时,先不给的 src 赋值,这样浏览器就不会发送请求了,等到图片进入可视区域再给 src 赋值

  • 图片的 src 不设置真实的路径
  • 图片的真实路径设置在其他属性中比如 data-src
  • 通过 js 判断图片是否进入可视区域
  • 如果图片进入可是区域将图片 src 换成真实路径

实现

其他方式已经大致实现懒加载,但是,它们都有一个缺点,就是一当发生滚动事件时,就发生了大量的循环和判断操作判断图片是否可视区里。这自然是不太好的,那是否有解决方法。
这里就引入了一个叫 Intersection Observer 观察器接口,它是是浏览器原生提供的构造函数,使用它能省到大量的循环和判断。当然它的兼容可能不太好,看情况使用。

Intersection Observer 是什么呢?这个构造函数的作用是它能够观察可视窗口与目标元素产生的交叉区域。
简单来说就是当用它观察我们的图片时,当图片出现或者消失在可视窗口,它都能知道并且会执行一个特殊的 回调函数,我们就利用这个回调函数实现我们的操作。
概念枯燥难懂,直接看下面例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const images = document.getElementsByTagName("img");

function callback(entries) {
for (let i of entries) {
if (i.isIntersecting) {
let img = i.target;
let trueSrc = img.getAttribute("data-src");
img.setAttribute("src", trueSrc);
observer.unobserve(img);
}
}
}

const observer = new IntersectionObserver(callback);

for (let i of images) {
observer.observe(i);
}

讲一下防抖节流

防抖节流本质上是优化高频率执行代码的一种手段
如:浏览器的 resizescrollkeypressmousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能
为了优化体验,需要对这类事件进行调用次数的限制,对此我们就可以采用 防抖(debounce)节流(throttle) 的方式来减少调用频率

定义

  • 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时
  • 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效

一个经典的比喻:

想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应
假设电梯有两种运行策略 debouncethrottle,超时设定为15秒,不考虑容量限制

电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送,这是防抖
电梯第一个人进来后,15秒后准时运送一次,这是节流

代码实现

loadsh 调库就完事了

普通函数,箭头函数的区别

  • 箭头函数没有原型,原型是 undefined
  • 箭头函数 this 指向全局对象,而函数指向引用对象
  • call、apply、bind 方法改变不了箭头函数的指向

Vuex 解决了什么问题?

  1. 多个组件依赖于同一状态时,对于多层嵌套的组件的传参将会非常繁琐,并且对于兄弟组件间的状态传递无能为力
  2. 来自不同组件的行为需要变更同一状态,

Vuex 中状态存储在那里?怎么改变它

存储在 state 中,改变 Vuex 中的状态的唯一途径就是显式地提交 (commit) mutation

Vuex 和 Pinia 的区别

vuex 变更状态必须显示的提交执行 commit mutations 里的方法

1
2
3
import { useStore } from 'vuex'
const vuexStore = useStore()
vuexStore.commit('setVuexMsg', 'hello juejin')

或者在 actions 中进行 mutations 修改 state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { createStore } from "vuex";
export default createStore({
strict: true,
// 全局state,类似于vue种的data
state() {
return {
vuexmsg: "hello vuex",
}
},
// 修改state函数
mutations: {
setVuexMsg(state, data) {
state.vuexmsg = data;
},
},
// 提交的mutation可以包含任意异步操作
actions: {
async getState({ commit }) {
// const result = await xxxx 假设这里进行了请求并拿到了返回值
commit("setVuexMsg", "hello juejin");
},
}
});

组件中使用 dispatch 进行分发 actions。

1
2
3
4
5
6
7
8
9
<template>
<div>{{ vuexStore.state.vuexmsg }}</div>
</template>

<script setup>
import { useStore } from 'vuex'
const vuexStore = useStore()
vuexStore.dispatch('getState')
</script>

Pinia 则可以直接修改状态,且调试工具 能够记录到每一次的变化

Pinia 可以调用 $patch 方法修改多个 state 中的值,当然也可以修改一个

1
2
3
4
5
6
7
8
9
10
11
12
13
import { storeA } from '@/piniaStore/storeA'
const piniaStoreA = storeA()
console.log(piniaStoreA.name); // xiaoyue
piniaStoreA.$patch({
piniaMsg: 'hello juejin',
name: 'daming'
})
// 也可以使用函数的方式进行修改状态
// cartStore.$patch((state) => {
// state.name = 'daming'
// state.piniaMsg = 'hello juejin'
// })
console.log(piniaStoreA.name);// daming

也可以在 actions 中修改状态

Vuex 不同,Pinia 移除了 mutations,所以在 actions 中修改 state 就和 Vuexmutations 修改 state 一样。
实这也是我比较推荐的一种修改状态的方式,就像上面说的,这样可以实现整个数据流程都在状态管理器内部,便于管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { defineStore } from "pinia";
export const storeA = defineStore("storeA", {
state: () => {
return {
piniaMsg: "hello pinia",
name: "xiao yue",
};
},
actions: {
setName(data) {
this.name = data;
},
},
});

在组件中调用也不在需要 dispatch 函数,直接调用 store 的方法即可。

1
2
3
import { storeA } from '@/piniaStore/storeA'
const piniaStoreA = storeA()
piniaStoreA.setName('daming')

Pinia 可以使用 $reset 将状态重置为初始值。

1
2
3
import { storeA } from '@/piniaStore/storeA'
const piniaStoreA = storeA()
piniaStoreA.$reset()

getters

其实 Vuex 中的 getters 和 pinia 中的 getters 用法是一致的,用于自动监听对应 state 的变化,从而动态计算返回值(和 vue 中的计算属性差不多),并且 getters 的值也具有缓存特性。

modules

如果项目比较大,使用单一状态库,项目的状态库就会集中到一个大对象上,显得十分臃肿难以维护。所以 Vuex 就允许我们将其分割成模块(modules),每个模块都拥有自己 state、mutations、actions…。而 Pinia 每个状态库本身就是一个模块。

Pinia 没有 modules,如果想使用多个 store,直接定义多个 store 传入不同的 id 即可,如:

1
2
3
4
import { defineStore } from "pinia";
export const storeA = defineStore("storeA", {...});
export const storeB = defineStore("storeB", {...});
export const storeC = defineStore("storeB", {...});

总结:

Vuex 变更 state 状态必须显示的提交执行 commit mutations 里的方法,或者可以在 actions 中进行 commit mutations 修改 state,组件里则调用 dispatch('xxx') 分发 actions

Pinia 变更状态直接修改,引入 store 直接点对应属性(但是不建议这么搞,最好对应的数据流程变更都在状态管理器内部,这样更好管理)

另外 Pinia 移除了 mutations,修改状态放到了 actions 里,外部组件调用引入 `store 后直接点就可以了

getters 上两者都一样,都是自动监听 state 的变化,从而动态计算返回值,和计算属性差不多

Pinia 同时也没有 modules 属性,如果想使用多个 store,直接定义多个 store 传入不同的 id 即可

Vuex 的 modules 属性一般写在总的入口 index.js 内,里面为 modules 文件里的各个 module

组件使用中 Vuex 需要 vuexStore.state.moduleA.count

1
2
3
import { useStore } from 'vuex'
let vuexStore = useStore()
console.log(vuexStore.state.moduleA.count) // 1

而 Pinia 则直接引用具体的 module ,然后调用 module 里面属性

1
2
3
import { useUserStore } from '@/store/modules/user';
const userStore = useUserStore();
userStore.xxx

promise

promise 有几种状态

pending:等待状态

resolved:完成状态,调用 resolved() 后会进入 then

rejected:失败状态,调用 rejected() 后会进入 catch

promise 状态是否可变

不可变,一旦调用了 resolved() 或者 rejected() 则会进入对应的 then 或者 catch

promise 如何解决地狱回调

如果每个请求有依赖关系就给每个请求包一个 promise ,then 里面可以 return 一个 promise 来防止地狱回调,然后 .then() 链式调用

promise 有哪些方法?应用场景是什么?(all、race)

all 接受一个数组,数组里面是 promise,等待所有 promise 执行 resolved 才走 then,如果 resolved 有参数则 then 返回一个结果集,如果有一个 rejected 他就会走 catchrejected 优先级比 resolved

new promise 中书写两个函数他们是怎么执行的

同步执行

说一说 eventLoop(事件循环)宏任务与微任务

浏览器的事件循环: 执行 js 代码的时候,遇见同步任务,直接推入调用栈中执行,遇到异步任务,将该任务挂起,等到异步任务有返回之后推入到任务队列中,当调用栈中的所有同步任务全部执行完成,将任务队列中的任务按顺序一个一个的推入并执行,重复执行这一系列的行为被称为事件循环。 异步任务又分为宏任务和微任务。

  1. 先执行执行栈中的同步任务
  2. 异步任务(回调函数)放入任务队列中
  3. 一旦执行栈中的所有同步任务执行完毕,系统就会按顺序读取任务队列中的异步任务,被读取的异步任务结束等待进入执行栈中执行

宏任务:任务队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列。
微任务:等宏任务中的主要功能都完成后,渲染引擎不急着去执行下一个宏任务,而是执行当前宏任务中的微任务
宏任务包含: 执行 script 标签内部代码、setTimeout / setInterval、ajax请求、postMessageMessageChannel、setImmediate,I/O(Node.js)
微任务包含: Promise、MutonObserver、Object.observe、process.nextTick(Node.js)

打包优化

去除 console.log、关闭 sourceMap(默认关闭)cdngzip压缩(别忘了在 nginx 中配置)

vite 为什么比 webpack 快

webpack 是先打包再启动开发服务器,vite 是直接启动开发服务器,然后按需编译依赖文件。

  • webpack 先打包,再启动开发服务器,请求服务器时直接给予打包后的结果;
  • vite 直接启动开发服务器,请求哪个模块再对哪个模块进行实时编译;
  • 由于现代浏览器本身就支持 ES Modules,会主动发起请求去获取所需文件。vite 充分利用这点,将开发环境下的模块文件,就作为浏览器要执行的文件,而不是像 webpack 先打包,交给浏览器执行的文件是打包后的;
  • 在 HRM 方面,当某个模块内容改变时,让浏览器去重新请求该模块即可,而不是像webpack重新将该模块的所有依赖重新编译;
  • 当需要打包到生产环境时,vite 使用传统的 rollup 进行打包,所以,vite 的优势是体现在开发阶段,另外,由于 vite 使用的是 ES Module,所以代码中不可以使用 CommonJs

vue 插槽 slot

slot是什么

插槽分为 具名插槽,匿名插槽,作用域插槽,在子组件中定义 <slot> 标签,这样就形成一个占位,

其中匿名插槽也是默认插槽,不指定 name 属性

1
2
3
4
5
6
<!-- 子组件 -->
<template>
<slot>
<p>插槽后备的内容</p>
</slot>
</template>
1
2
3
4
<!-- 父组件 -->
<Child>
<div>默认插槽</div>
</Child>

具名插槽需要在 <slot name="xxx"></slot> 标签中指定 name

在父组件中使用

1
2
3
4
<!-- 父组件 -->
<Child>
<div #xxx>具名插槽</div>
</Child>

还有个作用域插槽通过 slot 传参

1
2
3
4
<!-- 子组件内 -->
<div class="base-content">
<slot name="header" :innerContent="22222222"></slot>
</div>
1
2
3
4
<!-- 父组件内: -->
<Child>
<template v-slot:header="headerData">{{ headerData.innerContent }}</template>
</Child>

vue 中的 data 为什么是一个函数

类比与引用数据类型。如果不用 function return 每个组件的 data 都是内存的同一个地址,那一个数据改变其他也改变了,这当然就不是我们想要的。用 function return 其实就相当于申明了新的变量,相互独立,自然就不会有这样的问题;js 在赋值 object 对象时,是直接一个相同的内存地址。所以为了每个组件的 data 独立,采用了这种方式。

keep-alive 的声明周期执行

页面第一次进入,钩子的触发顺序
created -> mounted -> activated
退出时触发 deactivated 当再次进入(前进或者后退)时,只触发 activated

事件挂载的方法等,只执行一次的放在 mounted 中;组件每次进去执行的方法放在 activated

watch 和 computed 的区别是

相同点:他们两者都是观察页面数据变化的。

不同点:computed 只有当依赖的数据变化时才会计算, 当数据没有变化时, 它会读取缓存数据。 watch 每次都需要执行函数。watch 更适用于数据变化时的异步操作。

当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。这是和 computed 最大的区别,请勿滥用。

实现水平垂直居中的几种方式

1
2
3
<div class="outer">
<div class="inner">hello world</div>
</div>

最常见的就是 flex 布局

1
2
3
4
5
.outer {
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
}

兼容性好的 absolute + margin: 0 auto

1
2
3
4
5
6
7
8
.outer {
position: relative;
}

.inner {
position: absolute;
margin: 0 auto;
}

缺点:需要固定居中元素的宽高,否则其宽高会被设为 100%(副作用)。

兼容性:

IE 6+, Chrome 4+, Firefox 2+, Android 2.3+, iOS 6+