vue面试题
1. Vue2与Vue3响应式原理?
Vue 2 使用的是基于 Object.defineProperty 的响应式系统。这意味着 Vue 会使用这个方法来劫持对象属性的 setter 和 getter,从而在属性被访问或修改时能够执行特定的逻辑。
性能:Object.defineProperty需要为每个属性单独设置监听器,这在对象属性较多时效率较低。
Vue 3使用Proxy 对象来拦截对象属性的读取和赋值操作,从而实现响应式。这意味着当对象的属性被访问或修改时,Vue能够自动追踪到这些变化,并通知视图进行更新。
性能:Proxy可以直接监听整个对象及对象属性的添加和删除
2. Vue2和Vue3的区别有哪些?
响应式系统重构
-
Vue 2:使用 Object.defineProperty 劫持属性,需手动处理新增属性(如 Vue.set),且无法监听整个对象。
-
Vue 3:基于 ES6 Proxy 代理整个对象,自动追踪属性增删,性能更优且无需额外操作。
Composition API 替代 Options API
-
Vue 2:采用 Options API,逻辑分散在 data、methods 等选项中,大型组件可读性差。
-
Vue 3:引入 Composition API,通过 setup() 函数和 ref、reactive 等工具按逻辑聚合代码,支持复用和 TypeScript 集成。
生命周期钩子变化
Vue3 兼容 Vue2 大部分生命周期,但在 Composition API 中命名有调整,且新增 setup 替代 beforeCreate/created
| Vue2 生命周期 | Vue3 生命周期 | 说明 |
|---|---|---|
| beforeCreate | setup() | 实例初始化后,数据观测、事件配置前调用 |
| created | setup() | 实例创建完成,可访问数据和方法,但 DOM 未生成 |
| beforeMount | onBeforeMount | 挂载开始前调用,模板已编译但未渲染到页面 |
| mounted | onMounted | DOM 已挂载完成,可操作 DOM 元素 |
| beforeUpdate | onBeforeUpdate | 数据更新导致虚拟 DOM 重新渲染前调用 |
| updated | onUpdated | 虚拟 DOM 重新渲染并应用到真实 DOM 后调用 |
| beforeDestroy | onBeforeUnmount | 实例销毁前调用,仍可访问实例 |
| destroyed | onUnmounted | 实例销毁完成,所有指令和事件监听已移除 |
pinia替代Vuex
-
移除了 Vuex 中繁琐的
mutations,直接通过actions修改状态(支持同步 / 异步)。 -
无需嵌套模块,通过创建多个
store实现模块化,结构更清晰。 -
原生支持 TypeScript,类型推导更友好,无需额外配置。
-
体积更小(约 1KB),API 更简洁,学习成本低。
-
兼容 Vue2/Vue3,且适配 Composition API/Options API 两种写法。
-
使用方法:
state:状态:相当于 Vue 组件的 data,返回初始状态;getters计算属性:相当于 Vue 组件的 computed,基于 state 派生新值; -
页面导入定义好的
store import { useCounterStore } from '@/stores/counter';获取 store 实例(Pinia 会自动管理单例,多次调用也返回同一个实例)const counterStore = useCounterStore()
vuex的属性: state(存储全局共享的变量) 如何使用
// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
// 1. 安装Vuex插件
Vue.use(Vuex)
// 2. 创建并导出Vuex仓库实例
export default new Vuex.Store({
// 状态仓库:存储全局共享数据
state: {
userInfo: null, // 全局用户信息
count: 0, // 测试用的数字
},
// 唯一修改State的地方:同步操作
mutations: {
// 方法名自定义,第一个参数固定是state(当前仓库的状态)
// 第二个参数payload:组件调用时传递的参数(可选)
SET_COUNT(state, payload) {
state.count = payload
},
ADD_COUNT(state) {
state.count++
},
// 设置用户信息
SET_USERINFO(state, user) {
state.userInfo = user
}
},
// 处理异步:不能直接改State,需commit调用mutations
actions: {
// 方法名自定义,第一个参数固定是context(仓库的上下文,包含state/mutations/commit等)
// 第二个参数payload:组件传递的参数(可选)
// 示例1:异步修改count(模拟定时器)
asyncSetCount(context, payload) {
setTimeout(() => {
// 调用mutations的方法:context.commit('mutations方法名', 参数)
context.commit('SET_COUNT', payload)
}, 1000)
}
},
// 对State数据二次处理:缓存结果,依赖不变则不重新计算
getters: {
// 方法名自定义,第一个参数固定是state
doubleCount(state) { // 计算count的两倍
return state.count * 2
},
isLogin(state) { // 判断用户是否登录
return !!state.userInfo
}
},
// 模块化:大型项目使用,后续单独讲
modules: {}
})
//页面使用vuex
//修改state的值
this.$store.state.userInfo.name == '张三' //可以直接修改
//使用mutations修改state里的值
1.基础写法,同步修改:通过$store.commit('mutations方法名', 参数)
this.$store.commit('SET_COUNT', num)
2.辅助函数写法
import { mapState, mapMutations } from 'vuex'
computed: {
...mapState(['userInfo', 'count'])
}
2.methods: {
...mapMutations(['SET_COUNT']),
}
<button @click="SET_COUNT(10)">同步设置为10</button>
vuex不是持久化存储的,如何实现持久化存储? 使用 vuex-persistedstate 插件
plugins: [
createPersistedState({
key: 'my-vuex-store', // 本地存储键名,自定义
storage: window.localStorage, // 选localStorage,永久保存
// 核心:paths指定要持久化的状态,格式为「模块名/状态名」
paths: [
'user/token', // 持久化user模块的token
'user/userInfo', // 持久化user模块的userInfo
'cart/cartList' // 持久化cart模块的cartList
// 根仓库的状态直接写:比如'appName'
]
})
]
Vuex 持久化的核心是将状态同步到 localStorage/sessionStorage,刷新后从本地读取恢复,解决内存状态丢失问题。
性能优化:
Tree-shaking:Vue 3 模块化设计支持 Tree-shaking,减少未使用代码,打包体积更小。
渲染优化:编译器重构减少 DOM 操作,虚拟 DOM 算法提升效率,实测性能提高。
Vue3 支持多个根节点(Fragment),无需外层包裹 div,Vue2不支持
3. Vue组件间通信方式?$emit(vue2)
-
父子组件通信:props + defineEmits:父组件通过props传递数据,子组件通过defineEmits触发事件向父组件传递数据,符合单向数据流原则。
-
ref + defineExpose:父组件通过ref获取子组件实例,子组件通过defineExpose暴露方法或属性。
-
跨级组件通信:provide/inject:父组件通过provide [pro:vaid]提供数据,任意层级子组件通过inject注入使用,适用于深层嵌套场景。
-
兄弟组件通信:EventBus:通过第三方库(如mitt)创建事件中心,组件间通过事件触发和监听通信。
-
Pinia/Vuex:全局状态管理工具,适合复杂应用中共享状态。
4.computed和watch的区别,什么时候用computed,什么时候用watch?
Computed: 必须返回一个值;有缓存,依赖不变时不重新计算;不支持异步操作;模板中需要渲染的数据
Watch: 没有返回值,执行副作用操作;无缓存,每次触发都会执行;支持异步操作;数据变化时需要执行特定操作;
5.Vue3中ref为什么要封装一层.value?
在Vue3中,ref需要通过.Value来访问和修改值。
响应原理:Vue3使用proxy实现响应式,但proxy无法直接代理原始值,ref通过将原始值包装在对象中,使其能被响应式追踪。对于对象类型,推荐reactive,不需要.value访问
6.Vue2中为什么不能同时使用v-if和v-for?
优先级问题: v-for 的优先级高于 v-if,这意味着 Vue 会先执行循环,再对每个循环项进行条件判断
7.Vue3中如何创建和使用自定义指令?请举例说明。
directive :Vue 提供的注册自定义指令的方法,接收两个参数:指令名称、指令的配置对象
创建自定义指令
app.directive('focus', {
mounted(el) {
el.focus();
}
});
<template><input v-focus /></template>
8.Vue组件封装?
封装组件指的是将一段可复用的代码封装成一个单独的模块,使得该模块可以在多个地方被重复使用,而不需要每次都重新编写。传参、事件和插槽是封装组件时常用的三种方式,通过这些方式可以让组件更加灵活和具有通用性。同时,组件的封装还包括对于内部实现逻辑的封装,使得外部使用者无需了解组件内部的细节,只需要使用组件提供的接口即可完成相应的功能。
插槽: 将父组件中的内容注入到子组件模板的指定位置。简单来说,插槽为子组件提供了一个“占位符”,父组件可以在这个占位符里填充任何模板代码(包括 HTML、其他组件等)
匿名插槽(插槽没有名字)、具名插槽(插槽有名字)、作用域插槽(传值)
什么情况下用插槽?
组件中有公用的,有部分内容别的父组件不需要,就用到了插槽。
需要多个插槽就用到了具名插槽。
<!-- 子组件 Child.vue - 具名插槽版 -->
<template>
<div class="child">
<h4>子组件的多个具名插槽</h4>
<!-- 具名插槽 header:绑定 title 参数 -->
<slot name="header" :title="pageTitle"></slot>
<!-- 具名插槽 content:绑定 list 参数 -->
<slot name="content" :list="goodsList"></slot>
</div>
</template>
<script>
export default {
data() {
return {
pageTitle: "商品列表", // 传给header插槽的参数
goodsList: ["苹果", "香蕉", "橙子"] // 传给content插槽的参数
};
}
};
</script>
<!-- 父组件 Parent.vue - 具名作用域插槽 -->
<template>
<div>
<Child>
<!-- 具名插槽header:接收title参数(解构写法) -->
<template v-slot:header="{ title }">
<h2>{{ title }}</h2>
</template>
<!-- 具名插槽content:接收list参数(解构写法) -->
<template v-slot:content="{ list }">
<ul>
<li v-for="item in list" :key="item">{{ item }}</li>
</ul>
</template>
</Child>
</div>
</template>
-
子组件是传参方:通过
<slot :属性名="内部数据" name="插槽名(可选)">绑定要传递的参数; -
父组件是接收方:通过
v-slot:插槽名="作用域对象"接收,默认插槽可简写v-slot="对象"; -
开发首选解构接收:
v-slot="{ 属性1, 属性2: 重命名 }",代码更简洁,是 Vue 官方推荐写法; -
具名作用域插槽必须用
<template>包裹内容,再绑定v-slot。
9.在Vue项目如何实现动态菜单权限和按钮级权限?
菜单权限:
(1)后端按用户角色返回可访问菜单列表,前端基于路由表做过滤&渲染,同时拦截非法路由跳转。
(2) router.addRoute动态注入路由(避免白屏)。
(3)通过路由守卫router.beforeEach拦截,判断目标路由是否在“可访问路由”中,无权限则跳转404/登录页。
按钮级权限(核心是“权限标识+自定义指令”)
(1)权限标识约定:后端返回用户权限标识(如sys:user:add、sys:user:edit),存储在Pinia/Store。
(2)自定义指令实现(Vue 3):
封装v-permission指令,传入按钮所需权限标识,指令内部判断是否有权限,无权限则移除DOM/设置禁用(注册指令:
app.directive('permission', permissionDirective);)
(3)组件中使用:直接在按钮上绑定指令,传入对应权限标识:
<el-button type="primary" v-permission="'sys:user:add'">新增用户</el-button>
路由方法:路由跳转、传参、路由守卫、注入路由
<router-link :to="{ name: 'User', params: { id: 123 }}">用户页面</router-link>
<router-link :to="{ path: '/search', query: { keyword: 'vue' }}">搜索</router-link>
router.push({
name、path: 'User',
params、query: { id: 123 }//params作为URL路径的一部分,位于路径段中。query附加在URL末尾,以?开头,键值对用&连接。
});
router.replace('/about');//替换当前路由(不添加历史记录);
router.go(-1) // 后退、前进
10.Vite和Webpack的区别?
Webpack需要先打包构建整个项目的依赖和源码,生成打包后的 bundle,然后服务器才能提供服务。当一个文件变化时,Webpack 需要重新计算依赖、打包。速度慢
Vite基于原生 ESModules,无需打包。它直接将源码作为原生 ES 模块交给浏览器。 当一个文件变化时,只重新编译和传输变化的模块。速度快。
Vite配置: 1.入口文件(entry);2.开发服务器配置(serve);3.构建配置(build);4.插件配置(plugins);5.路径别名(resolve);
11.Vue3项目打包时如何打包成不同环境下的包文件?
(1)环境变量配置:在项目根目录创建 .env.dev (开发)和.env.pro (生产)文件
.env.pro(生产环境):VITE_BASE_URL= prod.xxx.com
(2) package.json命令配置,通过 --mode 指定环境,输出到不同目录(用 --outDir 指定输出路径):"build:pro": "vite build --mode pro --outDir dist-pro";
(3) vite.config.js 配置:build(输出目录outDir:dist-${env.VITE_ENV});
12.为什么pnpm比npm快?
npm同一个包在不同的项目中会被重复安装
pnpm在本地电脑上创建一个全局的存储仓库。当你安装一个包时,pnpm 会执行以下操作:
首先检查这个包是否已经存在于全局存储中。
如果存在,pnpm 不会复制文件,而是在项目的 node_modules 中创建一个硬链接,指向全局存储中的文件。
如果不存在,才从网络下载,并存入全局存储,然后创建硬链接。
13.如果让你从零搭建一个后台管理系统,你会怎么做?
(1)技术选型:Vite + Vue3 + Pinia + Element Plus(主流方案);
(2)工程化:配好ESLint代码规范,拆分环境变量;
(3)核心模块:权限路由(动态路由表+按钮级权限)、请求封装(拦截器+错误统一处理)、状态管理
(4)性能与安全:路由懒加载、接口防抖、XSS过滤(展现全局观);
(5)协作:用GitFlow管理分支,写清晰README和CHANGELOG(工程意识)。
git clone 远程仓库地址 # 例:git clone https://github.com/xxx/xxx.git
git pull # 快捷方式,等价于 git fetch + git merge
git branch # 查看本地分支(* 标记当前分支)
git branch -a # 查看本地+远程所有分支
git checkout -b 新分支名 # Git 2.23+ 也可用 git switch -c 新分支名
git branch -d 本地分支名 # 删除已合并的本地分支
git branch -D 本地分支名 # 强制删除未合并的本地分支
如将 dev 分支合并到 main 分支
- 切换到目标分支:
git checkout main(先确保目标分支是最新的,可先git pull) - 执行合并:
git merge 待合并分支名(例:git merge dev) - 解决合并冲突,冲突文件会标记
<<<<<<< HEAD(当前分支内容)、=======(待合并分支内容)、>>>>>>> 分支名,手动编辑保留需要的内容,删除冲突标记,然后:
git add 冲突文件名
git commit -m "解决合并冲突,合并dev到main"
git push
项目中遇到的难点,或是做项目有哪些亮点? 处理大批量的大文件上传,一次上传1万条
(1)安装依赖npm install ali-oss
(2)前端用过后端接口获取临时密钥AccessKey
(3)对单文件如>500MB按OSS要求拆分分片(默认最大为100MB,建议用5MB或10MB分片,平衡并发与请求书) 用client.multipartUpload内置的分片逻辑,或手动用slice切割Bolb对象,生成每个文件的唯一标识(FileMd5+文件名)用于断点续传。
(4)OSS核心办法client.multipartUpload传入文件的路径,并发上传的数量(parallel)progress监听分片上传进度,checkpoint断点续传(从localStorage读取已上传分片记录,中断后可续传),批量处理1万条文件用promise.allsettled替代promise.all(避免单个文件失败导致全部中断)配合“队列调度”每次只上传20个传完再取下一批,防止内存溢出。
(5)OSS会自动合并所有分片,无需前端处理,前端接收OSS返回的文件URL,前端将文件标识+OSSURL提交给后台。
注意事项:
(1) 分片编号:必须从 1 开始,且连续(如 1、2、3...),合并时需按编号顺序提交 ETag。
(2) 断点续传:通过保存checkpoint(包含 UploadId、已上传分片信息),可实现断点续传,避免重新上传已完成的分片。
(5) 过期分片清理:OSS 会保留未合并的分片 7 天,若无需合并,需调用abortMultipartUpload接口主动删除,避免产生存储费用。
(6) 并发上传:可通过多线程(服务端)或 Promise.all(前端)实现分片并发上传,提升效率,但需控制并发数(避免请求过多被限流)。
axios请求封装?
//src/utils/request),创建核心文件`index.js`,封装逻辑全部写在这里
import axios from 'axios'
import { ElMessage } from 'element-plus' // 示例:UI库的提示组件(可替换为自己项目的)
// 若用Vue2/ElementUI,替换为:import { Message } from 'element-ui'
// 若无UI库,可自定义alert提示
// 1. 创建Axios实例,配置基础参数
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // 接口基础地址(环境变量区分开发/生产)
timeout: 10000, // 请求超时时间:10秒
headers: {
'Content-Type': 'application/json;charset=utf-8' // 默认请求头
}
})
// 2. 请求拦截器:请求发送前做处理(如添加token、拼接参数)
service.interceptors.request.use(
(config) => {
// 示例:从本地存储获取token,添加到请求头(前后端约定的token字段,如Authorization/token)
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}` // JWT规范的token格式,可根据后端要求修改
}
// 其他请求前处理:如GET请求参数拼接、添加设备标识等
return config
},
(error) => {
// 请求配置错误的处理
Promise.reject(error)
}
)
// 3. 响应拦截器:响应返回后做统一处理(如解包数据、统一错误处理)
service.interceptors.response.use(
(response) => {
// 解构响应数据(后端返回的通用格式:{ code, msg, data },可根据实际后端格式修改)
const { code, msg, data } = response.data
// 成功码:后端约定的成功标识(如200/0/20000),根据实际修改
if (code === 200) {
return data // 只返回核心数据,简化业务层调用
} else {
// 业务错误:如参数错误、数据不存在(非网络错误)
ElMessage.error(msg || '请求失败')
return Promise.reject(new Error(msg || '请求失败'))
}
},
(error) => {
// 网络/系统错误:如404/500/超时/无网络
let errorMsg = '网络异常,请稍后重试'
if (error.response) {
// 有响应码:根据HTTP状态码做针对性提示
const status = error.response.status
switch (status) {
case 401:
errorMsg = '未登录/登录过期,请重新登录'
// 额外操作:清除token、跳转到登录页
localStorage.removeItem('token')
window.location.href = '/login'
break
default:
errorMsg = error.response.data?.msg || '请求失败'
}
} else if (error.request) {
// 有请求但无响应:超时/无网络
errorMsg = '请求超时,请检查网络'
}
// 统一提示错误信息
ElMessage.error(errorMsg)
// 抛出错误,让业务层可以catch做后续处理
return Promise.reject(error)
}
)
// 4. 封装通用的请求方法:get/post/put/delete,简化调用
/**
* 封装POST请求(JSON格式传参,默认)
* @param {string} url 接口地址
* @param {object} data POST请求体参数
* @returns {Promise}
*/
export const post = (url, data = {}) => {
return service({
method: 'post',
url,
data
})
}
// 导出Axios实例(如需自定义请求配置,可直接使用service)
export default service
//用户模块接口(src/api/user.js)
// 引入封装的请求方法
import { get, post, put, del } from '@/utils/request'
// 1. 获取用户列表
export const getUserList = (params) => {
return post('/user/list', params) // params:{ page: 1, size: 10, keyword: 'xxx' }
}
//在组件中调用接口(Vue3 示例)
import { getUserList } from '@/api/user';
const userList = ref([]);
// 获取用户列表
const getList = async () => {
try {
// 调用封装后的接口,直接获取核心数据(无需再解构response.data)
const res = await getUserList({ page: 1, size: 10 })
userList.value = res
} catch (err) {
// 可选:业务层单独处理错误(如特殊场景的错误提示)
console.log('获取列表失败:', err)
}
}
js面试题
1.null 和undefined的区别?
null 是无对象 (漏洞null和object共享了同一个类型标记)
undefined 是无值
2.箭头函数和普通函数的区别
- this绑定:普通函数的this指向调用它的对象,动态变化;箭头函数的this指向定义时的外层对象固定不变。
- 构造函数:普通函数可以使用new实例化对象;箭头函数不能使用new,无法创建实例。
- arguments对象:普通函数有arguments对象,可获取所有参数;箭头函数没有,需用剩余参数(...args)替代。
- 语法简洁性:箭头函数省略function、大括号和return,更简洁;普通函数语法完整,适合复杂逻辑 。
- 原型属性:普通函数有prototype属性;箭头函数没有,无法定义原型方法。
- super关键字:普通函数中super动态绑定父类方法;箭头函数中super继承外层作用域,不重新绑定。
1.js什么是浅拷贝、深拷贝?ES6
浅拷贝,相当于创建一个数据的快捷方式,快捷方式的数据改变原数据也改变。
方法:Object.assign();Array.prototype.slice() 或 Array.prototype.concat()(仅限数组)
深拷贝,创建一个新对象,并且递归地拷贝它内部的对象或数组。拷贝的数据改变原数据不会改变
方法:const copy = JSON.parse(JSON.stringify(original)); 递归函数
2.什么是原型和原型链?
原型(Prototype) :函数都有prototype属性称之为原型,因为这个属性的值是个对象,也称为原型对象,也称为原型对象。
作用: 原型可以放一些属性和方法,共享给实例对象使用; 在javascript中实现继承
__proto__:每个对象都有__proto__属性,作用:这个属性指向它的原型对象
原型链(Prototype Chain) :对象都有__proto__属性,这个属性指向它的原型对象prototype,原型对象也是对象,也有__proto__属性,指向原型对象的原型对象,这样一层一层形成的链式结构称为原型链,最顶层找不到则返回null
3.什么是作用域和作用域链?
作用域:是编程语言中定义的变量、函数、对象的可访问范围
作用域链:当访问一个变量时,JS 引擎会从当前作用域开始,沿着这条 “链” 依次向上查找,直到找到该变量或到达全局作用域
4.js的防抖、截流 setTimeout、clearTimeout
防抖:是指在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。避免重复点击。
截流:截流确保一个函数在指定的时间间隔内最多只执行一次。这意味着无论事件触发了多少次,函数只会在时间间隔的结束时执行一次。例如:滚动事件或窗口大小调整。
5.同步任务异步任务的区别?
同步任务: 顺序执行,前一个任务执行完,才能执行后一个任务。
异步任务: 乱序执行,发起异步任务后,立即执行后面的代码,不等待异步任务完成。
6.什么是事件循环?
因为JavaScript是单线程的,为了避免阻塞提出异步模型。异步模型是基于任务队列实现的,分为宏队列、微队列
同步优先,微任务(microtask)优于宏任务(macrotask),一次循环只取一个宏任务。
7.什么是宏任务,什么是微任务,async/await全部都是微任务吗?ES6
宏任务是浏览器或Node.js环境发起的异步任务,script 整体代码、setTimeout、setInterval、DOM 事件、AJAX 请求等;
微任务是JavaScript引擎内部发起的异步任务,如Promise回调、async/await等。
async/await本身是同步代码,但其内部的异步操作会生成微任务。
微任务会在当前宏任务结束后立即执行,且优先于下一个宏任务。
微任务:Promise的回调
Promise 的三种状态:pending(等待状态);fulfilled(已完成状态);rejected(已拒绝状态)。
Promise的的三种回调:then(处理成功的情况);catch(处理错误情况);finally(成功失败执行)回调
8.async/await的执行机制 [a: sing]
async函数本身是同步代码,但其内部的异步操作(如Promise)会生成微任务。
await会将后续代码(包括同步和异步部分)包装为微任务,等待当前宏任务结束后执行。
使用async/await +for循环实现逐个执行任务
9.浏览器输入URL地址,然后到页面展示会经历什么?
URL 解析、DNS 域名解析、建立 TCP 连接、发送 HTTP/HTTPS 请求、服务器处理请求并返回 HTTP 响应、浏览器解析和渲染页面、关闭 TCP 连接。
10.HTTPS加密过程?
握手阶段、证书验证、密钥协商、数据传输。
11.如何提高项目性能?
(1)资源压缩与合并:JS/CSS用Terser、CSSNano压缩,图片优先用WebP/AVIF(比JPG小50%+),小图直接转base64嵌入HTML避免请求。
(2) 分包:路由懒加载() => import();通过Webpack、Vite的splitChunks配置,将第三方库(如Vue、Pinia、Axios)或项目公共组件拆分为独立chunk,避免重复打包,提升缓存利用率。预加载、延迟加载
(3)关键资源预加载 预连接:首屏必需的 CSS/JS 用<link rel="preload">提前加载,跨域资源用<link rel="preconnect">提前建立连接
(4)非关键资源延迟加载:图片用loading="lazy",非首屏 JS 用defer(延迟执行,不阻塞 DOM 解析)或动态插入
(5)静态资源部署到CDN;
(6)避免无意义重渲染、优化 DOM 操作、使用虚拟列表;
(7)缓存计算结果、及时销毁定时器 / 事件;
(8)浏览器缓存、接口缓存、PWA 离线缓存。
12.图片懒加载?
Vue3 中使用Ant Design Vue组件库
<a-image width="200" :src="imageUrl" :lazy="true" /> //核心属性 `:lazy="true"`设置为 `true` 后,图片会在进入可视区域时才加载
原生: 通过监听scroll事件,结合getBoundingClientRect()检测图片是否进入视口,进入则替换src为data-src。
13.长列表如何渲染(大数据量渲染)?
使用虚拟列表只渲染用户能看到的区域,
成熟库:vue-virtual-scroller`` Vue-virtual-list;分页加载
14.什么是闭包?内存泄漏
闭包是指函数能访问作用域外部的变量
闭包会让外层函数的作用域(包含变量、参数)被内层函数持续引用,只要内层函数还存在,外层作用域就不会被回收。
15.页面打开很慢的问题?
网络传输拥堵(资源体积大js、css、图片、字体库)、网络请求过多、DOM结构过于复杂。
16.JavaScript数据类型有哪些?
- 栈:原始数据类型(Undefined、Null、Boolean、Number、String、Symbol、BigInt)
- 堆:引用数据类型(对象、数组和函数)
其中 Symbol 和 BigInt 是ES6 中新增的数据类型:
- Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
- BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。
17. 数据类型检测的方式有哪些
typeof
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof 'str'); // string
console.log(typeof []); // object
console.log(typeof function(){}); // function
console.log(typeof {}); // object
console.log(typeof undefined); // undefined
console.log(typeof null); // object
instanceof
console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
constructor
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
18. let、const、var的区别
(1)块级作用域: 块作用域由 { }包括,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:
- 内层变量可能覆盖外层变量
- 用来计数的循环变量泄露为全局变量
(2)变量提升: var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错。
(3)给全局添加属性: 浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。
(4)重复声明: var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。
(5)暂时性死区: 在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。
(6)初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。
(7)指针指向: let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。
17.数组有哪些原生方法?ES5
- 数组和字符串的转换方法:
toString()、toLocalString()、join()其中 join() 方法可以指定转换为字符串时的分隔符。字符串转数组splite()
const arr = [1, 2, 3, 'a', 'b'];
console.log(arr.join()); // 不传参,默认逗号分隔:1,2,3,a,b console.log(arr.join(',')); // 显式传逗号,和默认一致:1,2,3,a,b console.log(arr.join('-')); // 自定义分隔符-:1-2-3-a-b console.log(arr.join(' ')); // 空格分隔:1 2 3 a b console.log(arr.join('')); // 无分隔符,直接拼接:123ab
// 含undefined/null的情况
const arr2 = [1, null, 3, undefined, 5];
console.log(arr2.join('-')); // 1--3--5(null/undefined被转为空字符串)
const arr = [1, 2, 3, 'a'];
console.log(arr.toString()); // 1,2,3,a
// 嵌套数组
const arr2 = [1, [2, 3], [4, [5]]];
console.log(arr2.toString()); // 1,2,3,4,5(逐层扁平化拼接)
// 含空值
const arr3 = [null, undefined, [], 6];
console.log(arr3.toString()); // ,,,6
let str = "Hello World";
let arr = str.split(""); // 以空字符串作为分隔符
console.log(arr); // ["H", "e", "l", "l", "o", " ", "W", "o", "r", "l", "d"]
- 数组尾部操作的方法
pop()和push(),push()方法可以传入多个参数。 - 数组首部操作的方法
shift()和unshift() - 重排序的方法 reverse() 和 sort(),sort() 方法可以传入一个函数来进行比较,传入前后两个值,如果返回值为正数,则交换两个参数的位置。
// 数字数组反转
const numArr = [1, 2, 3, 4, 5];
const reversedNum = numArr.reverse();
console.log(numArr); // [5,4,3,2,1](原数组被修改)
console.log(reversedNum); // [5,4,3,2,1](返回原数组引用)
console.log(numArr === reversedNum); // true(同一个数组)
const numArr = [10, 2, 5, 1, 9];
// 数字升序(从小到大):
a - b numArr.sort((a, b) => a - b);
console.log(numArr); // [1, 2, 5, 9, 10]
// 数字降序(从大到小):
b - a numArr.sort((a, b) => b - a);
console.log(numArr); // [10, 9, 5, 2, 1]
- 数组连接的方法
concat(),返回的是拼接好的数组,不影响原数组。 - 数组截取办法
slice(),用于截取数组中的一部分返回,不影响原数组。
const arr = [10, 20, 30, 40, 50]; // 长度5,索引0-4
// 示例1:只传起始索引,截取到末尾
console.log(arr.slice(2)); // [30,40,50](索引2到末尾)
// 示例2:传起始+结束索引(左闭右开)
console.log(arr.slice(1, 4)); // [20,30,40](索引1、2、3)
- 数组插入方法
splice(),影响原数组
// 示例1:替换索引2的元素(删除1个,添加1个)
const arr = [10, 20, 30, 40, 50];
const rep1 = arr.splice(2, 1, 60);
console.log(rep1); // [30](被删除的原元素)
console.log(arr); // [10,20,60,40,50](60替换30)
const add1 = arr.splice(2, 0, 60);
console.log(add1); // [](无删除,返回空数组)
console.log(arr); // [10,20,60,30,40,50](60插入到索引2,原元素后移)
- 查找特定项的索引的方法,
indexOf()和lastIndexOf()
// 数字数组
const numArr = [1, 2, 3, 2, 1];
console.log(numArr.indexOf(2)); // 1(首次出现的索引)
console.log(numArr.indexOf(4)); // -1(没找到)
// 字符串数组
const strArr = ['a', 'b', 'c', 'b'];
console.log(strArr.indexOf('b')); // 1
console.log(strArr.indexOf('B')); // -1(严格区分大小写)
// 含空值的数组
const nullArr = [null, undefined, 0, ''];
console.log(nullArr.indexOf(null)); // 0
console.log(nullArr.indexOf(undefined)); // 1
// 数字数组:找2最后一次出现的位置
const numArr = [1, 2, 3, 2, 1, 2];
console.log(numArr.lastIndexOf(2)); // 5(最后一个2在索引5)
console.log(numArr.lastIndexOf(4)); // -1(未找到)
- 迭代方法 every()检查是否都符合条件、some()检查是否有一个符合条件、filter()、map() 和 forEach() 方法
- 数组归并方法 reduce() 和 reduceRight() 方法
18.for in和for of的区别
for...in 循环主要是为了遍历对象而生,不适用于遍历数组;
for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象
19. ES6有哪些新特性
1.箭头函数
2.解构赋值
3.模板字符串
4.promise
5.symbol Symbol是ES6中引入的一种新的基本数据类型,用于表示一个独一无二的值,不能与其他数据类型进行运算
6.新的变量声明方式-let和const
7.模块化-es6新增了模块化,根据功能封装模块,通过import导入,然后通过export导出也可以使用export default导出
8.for…of 循环,用于遍历可迭代对象(如数组、Map 和 Set)中的元素
9.扩展运算符:使用 ... 可以将数组或对象展开成多个参数,或者将多个参数合并成一个数组
10.展开运算符:在ES6中用...来表示展开运算符,它可以将数组或者对象进行展开
11.Map 和 Set,引入了两种新的数据结构,分别用于存储键值对和唯一值
12.Proxy,允许在对象和函数调用等操作前后添加自定义的行为
13.类(Class),引入了面向对象编程中类的概念
14.默认参数(Default Parameter),在定义函数时可以给参数设置默认值
users.find(u => u.id === 2); //查找对象{ id: 2, name: 'Bob' }
users.findIndex(u => u.id === 2); //查找对象索引1
arr.findLast(item => item === 2) //从数组末尾开始查找,返回第一个匹配的元素 2
arr.findIndexLast(item => item === 2); //从数组末尾开始查找,返回第一个匹配的元素的索引。
fruits.includes('banana') //一个数组是否包含一个指定的值,返回true、false
of() 创建一个具有可变数量参数的新数组实例
const arr1 = Array.of(7); // [7] new Array(7);
fill() 用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。
new Array(3).fill(0); // [0, 0, 0]
flat() const nested = [1, 2, [3, 4, [5]]]; console.log(nested.flat(2)); // [1, 2, 3, 4, 5] (深度为2)
flatMap() const words = sentences.flatMap(sentence => sentence.split(' '));
遍历和转换:优先使用 map, filter, forEach。
查找对象:使用 find 和 findIndex 代替 for 循环。
检查存在性:使用 includes 代替 indexOf > -1。
处理嵌套数组:使用 flat 和 flatMap。