vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > vue+js基础面试题

vue+js常见基础面试题及答案总结大全

作者:AllinLin

Vue.js是一个开源的JavaScript框架,用于构建用户界面,它通过自定义元素实现数据驱动和组件化的开发方式,旨在使开发过程更加直观和高效,这篇文章主要介绍了vue+js常见基础面试题及答案总结的相关资料,需要的朋友可以参考下

一、* ref和reactive的区别**

1、定义变量的区别

**ref:**主要用于定义基本数据类型(String、Number、Boolean、Null、Undefined),也可以定义引用数据类型(Object、Array 等)。

**reactive:**仅用于定义引用数据类型(Object、Array、Map、Set 等),不能直接定义基本数据类型(否则会报错或失去响应性)。

2、取值 / 修改值的区别

**ref:**定义的数据会被包装成一个带 value 属性的 Ref 对象,因此:在 JavaScript 中,必须通过 .value 取值或修改值;在 模板中,Vue 会自动解包(无需 .value)。

**reactive:**定义的数据会被包装成一个 ** Proxy 代理对象 **,可以直接通过属性名取值或修改值,无需额外语法

3、对于引用数据类型,ref和reactive定义方式的差异

**ref 定义引用类型:**直接将对象 / 数组作为参数传入 ref(),Vue 会自动将其转换为响应式数据。

**reactive 定义引用类型:**同样接收对象 / 数组作为参数,但要求必须是引用类型(传入基本类型会失效)。

4、内部处理机制的差异

**ref 对引用类型的处理:**ref会先将引用类型数据包装为 Ref对象(含 .value 属性),而 .value的值会被自动转换为 reactive 代理对象。即:ref({…}) 等价于 ref(reactive({…}))。

**reactive 对引用类型的处理:**直接返回 Proxy 代理对象,不经过 Ref 包装,直接对原始对象进行代理。

5、取值 / 修改值的差异

**ref 定义的引用类型:**在javascript中通过.value取值和重新赋值,不改变变量的响应性。在模板中直接访问不需要.value

reactive 定义的引用类型:在javascript中直接取值和重新赋值,在模板中也是直接访问,如果直接赋值,会丢失响应性(因为代理对象被覆盖)

6、重新赋值的影响

在 Vue3 中,ref 和 reactive 处理引用类型时,重新赋值和解构操作的表现差异,本质源于两者内部响应式实现机制的不同(ref 基于 Ref 包装对象,reactive 基于 Proxy 代理)。

**ref 定义的引用类型:**重新赋值后仍保持响应性

ref 包装的引用类型数据,其响应性由外层的 Ref 对象 保证(而非内部的具体值)。重新赋值时,只是替换了 ref.value 的内容,外层 Ref 对象的响应式能力不变。

原理:ref 的响应性绑定在 value 属性上,无论 value 指向什么值(原始值或引用值),只要通过 .value 操作,就能被 Vue 感知。

**reactive 定义的引用类型:**重新赋值后丢失响应性

reactive 返回的是Proxy 代理对象,其响应性依赖于对原始对象的代理关系。若直接重新赋值,相当于将变量指向了一个新的普通对象(非代理对象),原有的代理关系被切断,响应性失效。

原理:reactive 的响应性绑定在初始创建的 Proxy 对象上,一旦变量指向新对象,新对象未被 Proxy 代理,自然无法触发响应式更新。

7、解构的影响(响应性是否保留)

解构操作(如 const { name, age } = user)会提取对象的属性值,此时 ref 和 reactive 的响应性表现也不同:

**ref 定义的引用类型:**解构后需通过 .value 保持响应性

ref 包装的引用类型,其 .value 是一个 Proxy 对象(由 reactive 生成)。解构时若直接提取 .value 的属性,会得到普通值(丢失响应性);若保留 Ref 对象本身,则可通过 .value 维持响应性。

**reactive 定义的引用类型:**直接解构丢失响应性,需用 toRefs 转换

reactive 返回的 Proxy 代理对象,其属性是响应式的,但直接解构会将属性值转为普通值(脱离 Proxy 代理),导致响应性丢失。需通过 toRefs 将属性转为 Ref 对象,才能在解构后保持响应性。

import { reactive, toRefs } from 'vue'
 const user = reactive({ name: '张三', age: 20 })
  // 直接解构:属性变为普通值(非响应式)
 const { name, age } = user
 name = '李四'  // 不会触发视图更新
  // 正确方式:用 toRefs 转换后再解构(保持响应性)
 const { name: nameRef, age: ageRef } = toRefs(user)
 nameRef.value = '李四'  // 会触发视图更新(需 .value,因转为了 Ref 对象)

二、组件间传值

在 Vue3 中,组件间传值主要有 Props/Emits(父子)、Provide/Inject(跨层级)、Pinia(全局状态)、EventBus(跨组件,需手动实现)四种核心方式,具体用法如下:

一、父子组件传值(最常用)

父子组件是最直接的组件关系,传值方式也是 Vue 原生支持的核心方式。

1. 父组件 → 子组件:props

核心逻辑:父组件通过属性绑定的方式传递数据,子组件通过props选项声明接收,props 是只读的(不能直接修改)。

Vue2 示例

<!-- 父组件 Parent.vue -->
<template>
  <div>
    <!-- 通过属性传递数据 -->
    <Child :msg="parentMsg" :user="userInfo" />
  </div>
</template>
<script>
import Child from './Child.vue'
export default {
  components: { Child },
  data() {
    return {
      parentMsg: '我是父组件的消息',
      userInfo: { name: '张三', age: 20 }
    }
  }
}
</script>
<!-- 子组件 Child.vue -->
<template>
  <div>
    <p>{{msg }}</p>
    <p>{{user.name }} - {{ user.age }}</p>
  </div>
</template>
<script>
export default {
  // 声明接收的props(推荐指定类型+默认值,增强健壮性)
  props: {
    msg: {
      type: String,
      default: '默认消息' // 父组件未传值时使用
    },
   user: {
      type: Object,
     default: () => ({}) // 对象/数组的默认值需用函数返回,避免引用共享
    }
  }
}
</script>

Vue3 组合式 API 示例

<!-- 子组件 Child.vue -->
<script setup>
// 导入defineProps宏声明props
const props = defineProps({
  msg: {
    type: String,
   default: '默认消息'
  },
  user: {
    type: Object,
    default: () => ({})
  }
})
// 使用props:直接通过props.msg、props.user访问
console.log(props.msg)
</script>

2. 子组件 → 父组件:自定义事件($emit)

核心逻辑:子组件通过$emit触发自定义事件并传递数据,父组件监听该事件并接收数据。

Vue2 示例

<!-- 子组件 Child.vue -->
<template>
  <button @click="sendToParent">向父组件传值</button>
</template>
<script>
export default {
  methods: {
 	sendToParent() {
    // 触发自定义事件,第一个参数是事件名,后续是要传递的数据
       this.$emit('child-event', '我是子组件的消息', { id: 1 })
    }
  }
}
</script>
<!-- 父组件 Parent.vue -->
<template>
  <div>
    <!-- 监听子组件的自定义事件,通过$event接收单个参数,多参数用回调接收 -->
    <Child @child-event="handleChildData" />
    <p>接收的子组件数据:{{ childData }}</p>
  </div>
</template>
<script>
import Child from './Child.vue'
export default {
  components: { Child },
  data() {
    return {
      childData: ''
    }
  },
  methods: {
    handleChildData(msg, obj) {
      console.log(msg) // 输出:我是子组件的消息
      console.log(obj) // 输出:{ id: 1 }
      this.childData = msg
    }
  }
}
</script>

Vue3 组合式 API 示例

<!-- 子组件 Child.vue -->
<script setup>
// 导入defineEmits宏声明事件
const emit = defineEmits(['child-event'])
const sendToParent = () => {
  emit('child-event', '我是子组件的消息', { id: 1 })
}
</script>
<!-- 父组件 Parent.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const childData = ref('')
const handleChildData = (msg, obj) => {
  childData.value = msg
}
</script>

二. 复杂场景:EventBus(事件总线,Vue2 常用)**

核心逻辑:创建一个空的 Vue 实例作为 “事件总线”,所有组件都可以通过这个实例触发 / 监听事件。

·Vue2 直接创建:

// bus.js
import Vue from 'vue'
export default new Vue()
// 兄弟A
import bus from './bus.js'
bus.$emit('bus-event', 'A的消息')
// 兄弟B
import bus from './bus.js'
bus.$on('bus-event', (data) => {
  console.log(data) // 接收A的消息
})

· Vue3 需手动实现(Vue3 移除了off,用 mitt 库更简单):

# 安装mitt
npm install mitt
// bus.js
import mitt from 'mitt'
const emitter = mitt()
export default emitter
// 兄弟A
import emitter from './bus.js'
emitter.emit('bus-event', 'A的消息')
// 兄弟B
import emitter from './bus.js'
emitter.on('bus-event', (data) => {
  console.log(data)
})

三、跨级 / 任意组件传值(祖孙、无关联组件)

1. 依赖注入(provide/inject):适合祖孙组件

核心逻辑:祖先组件用provide提供数据,所有子孙组件都可以用inject注入使用(无视层级)。

<!-- 祖先组件 GrandParent.vue -->
<script setup>
    import { provide, ref } from 'vue'
    const theme = ref('dark')
    // 提供数据(第一个参数是key,第二个是值)
    provide('theme', theme)
    // 提供方法(支持响应式)
    provide('changeTheme', () => {
      theme.value = theme.value === 'dark' ? 'light' : 'dark'
    })
</script>
<!-- 孙组件 GrandChild.vue -->

<script setup>
    import { inject } from 'vue'
    // 注入数据
    const theme = inject('theme', 'light') // 第二个参数是默认值
    // 注入方法
    const changeTheme = inject('changeTheme')
</script>

2. 状态管理(Vuex/Pinia):适合任意组件(推荐)

核心逻辑:集中管理所有组件的共享数据,任意组件都可以访问 / 修改,是大型项目的首选方案。

· Vue2 使用 Vuex:

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
  state: { // 共享数据
    count: 0
  },
  mutations: { // 修改state的唯一方式
    increment(state) {
      state.count++
    }
  },
  actions: { // 异步操作
    asyncIncrement({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    }
  },
  getters: { // 计算属性
    doubleCount: state => state.count * 2
  }
})
// 组件中使用
this.$store.state.count // 访
this.$store.commit('increment') // 修改
this.$store.dispatch('asyncIncrement') // 异步修改

· Vue3 使用 Pinia(替代 Vuex,更简洁):

# 安装Pinia
npm install pinia
// store/index.js
import { createPinia } from 'pinia'
export default createPinia()
// store/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  actions: { // Pinia无mutations,直接在actions修改state
    increment() {
      this.count++
    },
    asyncIncrement() {
      setTimeout(() => {
        this.count++
      }, 1000)
    }
  },
  getters: {
	doubleCount: (state) => state.count * 2
  }
})
// 组件中使用

<script setup>
	import { useCounterStore } from '@/store/counter'
    const counterStore = useCounterStore()
    console.log(counterStore.count) // 访问
    counterStore.increment() // 修改
</script>

总结

  1. 父子组件:优先用props(父传子) + $emit(子传父),这是 Vue 原生最基础、最推荐的方式;

  2. 兄弟组件:简单场景用父组件中转,复杂场景用 EventBus(Vue2)/mitt(Vue3);

  3. 跨级 / 任意组件:祖孙组件用provide/inject,大型项目 / 任意组件共享数据优先用 Vuex(Vue2)或 Pinia(Vue3)。

关键点:所有传值方式中,props 是只读的,不能直接修改;状态管理库适合共享数据较多的场景,避免过度使用 EventBus 导致代码难以维护。

三、Vue2和Vue3的生命周期钩子函数详解

Vue2 和 Vue3 的生命周期核心逻辑一致(从创建到销毁的阶段),但 Vue3 因 Composition API 新增了组合式钩子,同时保留了 Options API 钩子以兼容 Vue2。

一、核心区别:Options API vs Composition API

· Vue2:仅支持 Options API,钩子函数直接定义在组件选项中(如 created、mounted)。

· Vue3:

  1. 兼容 Vue2 的 Options API 钩子(用法完全一致);

  2. 新增 Composition API 钩子(需从 vue 导入,如 onMounted、onUnmounted),更灵活适配组合式代码。

二、生命周期阶段与对应钩子(按组件 “创建 → 挂载 → 更新 → 卸载” 的顺序,整理两者对应钩子:)

生命周期阶段Vue2 (Options API) 钩子Vue3 钩子(Composition API)核心作用说明
组件创建阶段beforeCreate无(直接在 setup 中执行)实例未初始化,无法访问 data/methods
created无(直接在 setup 中执行)实例初始化完成,可访问 data/methods,但 DOM 未生成
DOM 挂载阶段beforeMountonBeforeMount模板编译完成,DOM 未挂载到页面
mountedonMountedDOM 挂载完成,可操作页面 DOM
数据更新阶段beforeUpdateonBeforeUpdate数据变化,DOM 未重新渲染
updatedonUpdated数据变化,DOM 重新渲染完成
组件卸载阶段beforeDestroyonBeforeUnmount组件即将卸载,可清理定时器 / 事件监听
destroyedonUnmounted组件已卸载,实例资源完全释放

三、Vue3 新增钩子(针对特殊场景)

仅在 Composition API 中可用,解决 Vue2 未覆盖的场景:

· onActivated:缓存组件( 包裹)激活时触发(如从后台切回前台)。

· onDeactivated:缓存组件失活时触发(如切到其他组件)。

· onErrorCaptured:捕获子组件抛出的错误,可阻止错误向上传播(类似 Vue2 的 errorCaptured 选项,但需主动导入)。

四、示例:Vue3 Composition API 用法

需在 setup() 中使用(或 语法糖):

<script setup>
import { onMounted, onUnmounted } from 'vue'
// 组件挂载后执行
onMounted(() => {
  console.log('DOM 已挂载,可操作')
})
// 组件卸载前执行(清理资源)
onUnmounted(() => {
  console.log('组件即将卸载,清理定时器')
  clearInterval(timer)
})
</script>

四、详细介绍Vue中路由守卫

Vue 中的路由守卫(Navigation Guards)是一套用于监听路由导航过程的钩子函数,能在路由跳转的不同阶段进行拦截、判断或处理,核心作用是控制路由访问权限、处理页面跳转逻辑(如登录验证、页面缓存)等。

一、路由守卫的分类

根据作用范围和执行时机,主要分为3类:全局守卫路由独享守卫组件内守卫

1. 全局守卫(作用于所有路由)

定义在 router 实例上,对所有路由的导航都生效,常用 3 个钩子:

钩子函数执行时机核心用途
router.beforeEach路由跳转前(初始化时也会执行)全局登录验证、权限判断(如未登录拦截到登录页)
router.beforeResolve路由跳转前,但在组件内守卫和路由独享守卫之后执行补充全局逻辑(较少单独使用,优先级低于 beforeEach)
router.afterEach路由跳转完成后(DOM 已更新)页面标题修改、统计埋点、滚动条重置等(无 next 方法)

示例(全局登录拦截)

const router = createRouter({ /* 路由配置 */ })
// 全局前置守卫:未登录则拦截到登录页
router.beforeEach((to, from, next) => {
  const isLogin = localStorage.getItem('token') // 判断是否登录
  // 目标路由需要登录,且未登录 → 跳登录页
  if (to.meta.requiresAuth && !isLogin) {
    next('/login') 
  } else {
    next() // 正常放行
  }
})

2. 路由独享守卫(作用于单个路由)

定义在具体路由规则中,仅对当前路由生效,常用 beforeEnter(路由跳转前执行)。

const routes = [
  {
    path: '/user',
    component: User,
    meta: { requiresAuth: true }, // 自定义元信息(标记需登录)
    // 路由独享守卫:仅对 /user 路由生效
    beforeEnter: (to, from, next) => {
      const isAdmin = localStorage.getItem('role') === 'admin'
      // 仅管理员可进入 /user 路由
      if (isAdmin) {
        next()
      } else {
        next('/403') // 无权限跳403页
      }
    }
  }
]

3. 组件内守卫(作用于组件内部)

定义在路由组件中,仅对当前组件对应的路由生效,能更灵活地结合组件状态处理逻辑,常用 3 个钩子:

钩子函数执行时机核心用途
beforeRouteEnter组件创建前(路由跳转前)初始化组件数据(需通过 next(vm => {}) 访问组件实例 vm)
beforeRouteUpdate组件已创建,但路由参数变化时(如 /user/1 → /user/2)监听路由参数变化,更新组件数据(无需重新创建组件)
beforeRouteLeave组件销毁前(离开当前路由时)弹窗确认(如表单未保存时阻止离开)

示例(组件内守卫)

<script>
    <template>
      <div>用户详情</div>  
    </template>
<script>
export default {
  data() {
    return { formSaved: false } // 表单是否已保存
  },
  // 1. 进入组件前:初始化数据(需通过 next 访问 vm)
  beforeRouteEnter(to, from, next) {
    // 模拟接口请求用户数据
    fetch(`/api/user/${to.params.id}`)
      .then(res => res.json())
      .then(data => {
        // 通过 next 传递数据到组件实例
        next(vm => { vm.userInfo = data })
      })
  },
  // 2. 路由参数变化时:更新用户数据
  beforeRouteUpdate(to, from, next) {
    this.fetchUser(to.params.id) // 复用组件内方法更新数据
    next()
  },
  // 3. 离开组件前:确认表单是否保存

  beforeRouteLeave(to, from, next) {
    if (!this.formSaved) {
      if (confirm('表单未保存,确定离开吗?')) {
        next() // 确认后放行
      } else {
        next(false) // 取消则阻止离开
      }
    } else {
      next()
    }
  },

  methods: {
    fetchUser(userId) { /* 接口请求逻辑 */ }
  }
}
</script>

二、核心概念:to、from、next

所有路由守卫(除都接收 3 个参数,是控制导航的核心:

o next():正常放行,进入 to 路由。

o next(false):阻止导航,停留在当前路由。

o next(‘/path’) 或 next({ path: ‘/path’ }):跳转到指定路由,替换当前导航。

三、执行顺序(关键)

当触发一次路由跳转时,守卫的执行顺序固定,需避免逻辑冲突:

  1. 全局前置守卫(router.beforeEach)

  2. 路由独享守卫(beforeEnter)

  3. 件内前置守卫(beforeRouteEnter)

  4. 全局解析守卫(router.beforeResolve)

  5. 导航完成,DOM 更新

  6. 全局后置守卫(router.afterEach)

  7. (若组件已创建,参数变化时)组件内更新守卫(beforeRouteUpdate)

  8. (若离开当前组件)组件内离开守卫(beforeRouteLeave)

四、常见场景

五、路由

一、vue-router 基础认知

vue-router 是 Vue 官方配套的路由管理器,专门解决 Vue 单页应用中「无刷新切换页面」的问题。核心逻辑是将 URL 路径 与 Vue 组件 做映射,支持 Hash/History 两种路由模式,还提供动态路由、嵌套路由、路由守卫等高级功能,是 Vue SPA 开发的核心依赖。

版本关键对应(新手必看)

二、安装与基础配置(以 Vue 3 + vue-router 4 为例)

  1. 安装依赖
# Vue 3 + vue-router 4(推荐)
npm install vue-router@4
# 或
yarn add vue-router@4
# Vue 2 + vue-router 3(兼容旧项目)
npm install vue-router@3

2. 完整基础配置流程

步骤 1:创建路由规则文件(src/router/index.js)

// 导入 Vue Router 核心方法
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
// 导入需要映射的页面组件(推荐懒加载,优化首屏加载)
const Home = () => import('@/views/Home.vue')
const About = () => import('@/views/About.vue')
// 路由规则数组:path(URL路径) ↔ component(对应组件)
const routes = [
  // 重定向:访问根路径时,自动跳转到/home
  { path: '/', redirect: '/home' },
  {
    path: '/home',
    name: 'Home', // 路由命名(可选,方便编程式导航)
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }
]
// 创建路由实例
const router = createRouter({
  // 配置路由模式(二选一):
  // 1. Hash模式:createWebHashHistory()(默认,无需服务器配置)
  // 2. History模式:createWebHistory()(URL无#,需服务器配置)
  history: createWebHashHistory(),
  routes // 传入路由规则
})
export default router

步骤 2:挂载路由到 Vue 应用(src/main.js)

import { createApp } from 'vue'
import App from './App.vue'
import router from './router' // 导入路由实例
const app = createApp(App)
app.use(router) // 全局挂载路由

app.mount('#app')

步骤 3:在根组件中使用路由(src/App.vue)

<template>
  <div class="app">
    <!-- 路由链接:替代a标签,点击不刷新页面 -->
    <router-link to="/home">首页</router-link>
    <router-link to="/about">关于我们</router-link>
    <!-- 路由出口:匹配到的组件会渲染到这个位置 -->
    <router-view></router-view>
  </div>
</template>

三、vue-router 核心功能与示例

1. 两种路由模式配置(重点)

模式配置方法URL 示例优点缺点
Hash 模式createWebHashHistory()http://localhost/#/home兼容性好、无需服务端配置URL 带 #,不够美观
History 模式createWebHistory()http://localhost/homeURL 美观、符合用户认知仅支持 IE10+,刷新需服务端配置

History 模式的服务端配置(Nginx 示例):需将所有路由路径指向 SPA 入口文件(index.html),否则刷新会报 404:

Nginx:
server {
  location / {
    try_files $uri $uri/ /index.html; # 核心配置
  }
}

2. 动态路由(匹配可变路径)

用于「详情页」这类路径(如 /user/1、/user/2),通过 :参数名 定义动态段。

// 路由规则
const routes = [
  {
    path: '/user/:id', // :id 是动态参数(可多个,如 /user/:id/:name)
    name: 'User',
    component: () => import('@/views/User.vue')
  }
]
在组件中获取动态参数:
<template>
  <div>当前用户ID:{{ route.params.id }}</div>
</template>
<script setup>
// Vue3 组合式API:通过useRoute获取路由信息
import { useRoute } from 'vue-router'
const route = useRoute()
console.log(route.params.id) // 输出URL中的id(如1/2/3)
</script>

3. 嵌套路由(路由层级)

用于「父页面包含子页面」的场景(如首页包含「推荐」「热门」子模块),通过 children 配置嵌套路由。

// 路由规则
const routes = [
  {
    path: '/home',
    component: () => import('@/views/Home.vue'),
    // 嵌套路由:子路由path不要加/(除非是根路径)
    children: [
      { path: '', redirect: 'recommend' }, // 首页默认显示推荐子页面
      { path: 'recommend', component: () => import('@/views/HomeRecommend.vue') },
      { path: 'hot', component: () => import('@/views/HomeHot.vue') }
    ]
  }]
//父组件(Home.vue)需添加子路由出口:
<template>
  <div>
    <h1>首页</h1>
    <!-- 子路由链接 -->
    <router-link to="/home/recommend">推荐</router-link>
    <router-link to="/home/hot">热门</router-link>
    <!-- 子路由出口:子组件渲染到这里 -->
    <router-view></router-view>
 </div>
</template>

4. 路由传参(两种方式)

传参方式特点示例(路由链接)获取方式
params参数嵌入 URL,刷新不丢route.params.id
query参数以?拼接,刷新不丢route.query.name

5. 编程式导航(JS 控制路由跳转)

替代 ,通过代码触发路由跳转(如按钮点击):

<template>
  <button @click="goToAbout">跳转到关于页</button>
  <button @click="goBack">返回上一页</button>
</template>
<script setup>
	import { useRouter } from 'vue-router'
	const router = useRouter()
// 跳转指定路由
const goToAbout = () => {
  // 方式1:直接传路径
  router.push('/about')
  // 方式2:命名路由+参数(推荐,路径修改时只需改路由规则)
  // router.push({ name: 'About', query: { name: '张三' } })
}
// 浏览器历史回退
const goBack = () => {
  router.go(-1) // -1=上一页,1=下一页
}
</script>

**6. 路由守卫(路由拦截 / 权限控制)**同(四)详细介绍vue中的路由守卫

四、Vue2 + vue-router3 核心差异(兼容参考)

Vue2 的配置方式略有不同,核心区别如下:
// src/router/index.js(Vue2)
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter) // 注册路由
const routes = [/* 规则和Vue3一致 */]
const router = new VueRouter({
  mode: 'hash', // Hash模式(默认),History模式写mode: 'history'
  routes
})
export default router

六、Vue X 和 Pinia ,两者的区别以及 Pinia 的优势。

1. 核心概念介绍

· Vuex:Vue 官方早期推出的状态管理库,专为 Vue 2 设计,采用集中式存储管理应用的所有组件状态,核心依赖 Vue 实例。

· Pinia:Vue 官方推荐的新一代状态管理库,是 Vuex 的继任者,同时支持 Vue 2 和 Vue 3,API 更简洁,且深度适配 Vue 3 的 Composition API。

2. 核心区别

对比维度VuexPinia
模块划分需通过 modules 实现模块化,复杂项目中易出现嵌套冗余无模块概念,每个 store 就是一个独立模块,可直接嵌套组合,结构更清晰
状态修改必须通过 mutations 提交修改(同步操作),异步操作需在 actions 中调用 mutations,流程较繁琐无需 mutations,可在 actions 中直接修改状态(支持同步 / 异步),简化操作流程
类型支持对 TypeScript 支持较弱,需手动定义类型或借助辅助函数(如 mapState),易出现类型丢失原生支持 TypeScript,无需额外配置,类型推导精准,开发体验更优
API 复杂度API 较多(如 state/mutations/actions/getters/modules/namespaced),学习成本高API 极简(仅 state/actions/getters),无冗余概念,上手更快
Vue 版本适配主要适配 Vue 2,Vue 3 中需使用 Vuex 4,且仍保留 Vue 2 的设计思路同时适配 Vue 2 和 Vue 3,且针对 Vue 3 的 Composition API 做了优化,支持 setup 语法中直接使用

3. Pinia 的核心优势

  1. 更简洁的 API:移除 mutations,异步 / 同步操作统一在 actions 中处理,代码量减少约 40%,逻辑更直观。

  2. 原生 TypeScript 支持:无需额外配置,状态、操作、getter 均能自动推导类型,避免类型错误,提升大型项目可维护性。

  3. 更灵活的模块化:每个 store 独立存在,可直接导入使用,无需像 Vuex 那样通过 modules 注册和命名空间区分,降低嵌套复杂度。

  4. 更好的 Vue 3 适配:深度兼容 Composition API,可在 setup 中直接解构 store 状态,无需借助 mapState 等辅助函数,与 Vue 3 开发范式更契合。

  5. 体积更小:Pinia 包体积仅约 1KB(gzip 后),比 Vuex 更轻量,对项目性能影响更小。

**七、Vuex,localstorage以及sessionstorange和cookies的区别

1. 核心定位与本质区别

技术核心定位本质类型核心作用场景
Vuex/PiniaVue 生态的状态管理库内存中的逻辑层工具管理 Vue 应用内多组件共享的实时状态(如购物车、用户登录状态)
localStorage浏览器本地持久化存储 API客户端存储工具持久化保存不敏感数据(如主题偏好、历史记录)
sessionStorage浏览器会话级存储 API客户端存储工具临时保存当前标签页会话数据(如表单草稿、临时令牌)
Cookie浏览器小型存储 + HTTP 携带工具客户端存储工具传递服务端信息(如会话 ID)、保存少量配置(如记住登录状态)

2. 关键特性对比(5 大核心维度)

(1)生命周期

(2)存储容量

(3)数据共享范围

(4)是否随 HTTP 请求携带

(5)响应式支持

3.典型使用场景总结

八、详细介绍v-model的实现原理

v-model 本质是语法糖,核心通过v-bind绑定value属性和v-on监听input事件实现双向数据绑定,无需手动编写这两个步骤。

1. 底层实现逻辑(以输入框为例)

v-model 的完整等价代码如下,清晰展示其双向绑定的本质:

<!-- v-model 语法糖 -->
<input v-model="message" />
<!-- 等价于手动绑定 value + 监听 input 事件 -->
<input :value="message" @input="message = $event.target.value" />

2. 不同元素的适配规则

v-model 并非固定使用value和input,会根据表单元素类型自动调整绑定的属性和事件,以匹配元素原生特性:

3. 自定义组件中的实现

在自定义组件上使用 v-model 时,需遵循组件的约定:

  1. 组件内部通过props接收外部传递的value(默认属性名)。

  2. 组件内部触发input事件(默认事件名),并通过$emit(‘input’, 新值)将数据传递给父组件,完成双向同步。

如果需要修改默认的value和input,可通过组件的model选项自定义:

// 子组件
export default {
  model: {
    prop: 'checked', // 自定义接收的属性名,代默认的 value
    event: 'change'  // 自定义触发的事件名,替代默认的 input
  },
  props: ['checked'] // 接收父组件通过 v-model 传递的值
}

九、ES6语法糖

ES6(ECMAScript 2015)语法糖是简化代码书写、提升可读性的便捷语法,不改变语言核心功能,仅优化编码体验。以下是常用核心语法糖:

1. 变量声明:let/const

替代 var,解决变量提升、作用域混乱问题。

2. 箭头函数(Arrow Functions)

简化函数定义,自动绑定外层 this(无自身 this)。

3. 解构赋值(Destructuring Assignment)

快速从数组 / 对象中提取值,赋值给变量。

4. 模板字符串(Template Literals)

替代字符串拼接,支持多行文本和变量插入。

5. 扩展运算符(Spread Operator)

用 … 表示,可展开数组 / 对象,简化数据拷贝和合并。

6. 简化对象属性(Shorthand Object Properties)

当对象属性名与变量名相同时,可省略属性值的变量名。

7. 简化对象方法(Shorthand Object Methods)

对象方法定义可省略 function 关键字。

8. 数组方法简化(如 find、includes 等)

新增高频数组方法,替代传统循环逻辑。

十、Watch和computed区别

watch 和 computed 都是 Vue 中用于响应式处理的 API,但核心定位和使用场景完全不同,核心区别可概括为:computed 是 “计算属性”,专注于 “衍生值计算”;watch 是 “监听器”,专注于 “响应值变化执行副作用”

1. 核心用途不同(最本质区别)

2. 依赖处理与缓存机制不同

  1. 自动追踪依赖:computed 会自动识别其内部使用的响应式数据(如 firstName),只有依赖变化时,才会重新计算。

  2. 结果缓存:若依赖未变,多次访问 computed 属性会直接返回缓存结果,避免重复计算(提升性能)。

例:fullName() { return this.firstName + this.lastName },若 firstName 和 lastName 都没改,每次访问 this.fullName 都用缓存值。

  1. 手动指定监听目标:需明确告诉 watch 要监听哪个数据(如 watch: { userId: () => {} }),不自动追踪其他依赖。

  2. 无缓存:只要监听的目标数据变化,就会触发回调函数,每次变化都会执行一次,不存在 “复用结果” 的逻辑。

例:监听 userId 时,只要 userId 变了,就会重新执行请求逻辑,哪怕新 userId 和旧 userId 实际对应相同数据,也会重复执行。

3. 写法与返回值不同

computed: {
  fullName() {
    return this.firstName + ' ' + this.lastName; // 必须返回值
  }
}
watch: {
  userId(newVal) {
    // 支持异步(如 axios 请求)
    axios.get(`/api/user/${newVal}`).then(res => {
      this.userInfo = res.data;
    });
   }
}

4. 使用场景总结(如何选择)

一句话记忆: 想 “算个值”?用 computed; 想 “等变化做件事”?用 watch。

十一、ES6模块化

ES6 模块化是 ECMAScript 2015 引入的官方模块系统,通过import(导入)和export(导出)语法实现代码拆分与复用,解决全局作用域污染、依赖管理混乱等问题,是现代前端工程化的核心基础。

一、核心语法:导出(export)

用于对外暴露模块中的变量、函数、类等,分为命名导出默认导出两种方式。

1. 命名导出(Named Export)

// 导出变量
export const name = "ES6 Module";
// 导出函数
export function add(a, b) { return a + b; }
// 导出类
export class User { constructor(name) { this.name = name; } }

​ o 集中导出(适合整理多个成员):

const age = 20;
function multiply(a, b) { return a * b; }
// 集中暴露,名称需与内部变量/函数名一致
export { age, multiply };

2. 默认导出(Default Export)

// 导出默认函数
export default function(a, b) { return a - b; }
// 导出默认类
export default class { constructor(age) { this.age = age; } }

​ o 导出已声明的成员(需加default):

const message = "Hello Module";
// 注意:默认导出不能直接写 export default message; 外的大括号
export default message;

二、核心语法:导入(import)

用于引入其他模块导出的成员,需与导出方式对应(命名导出→命名导入,默认导出→默认导入)。

1. 导入命名成员(Named Import)

// 导入单个/多个命名成员
import { name, add } from "./module.js"; // 路径需正确(相对路径需加 ./)
// 重命名导入(如避免与本地变量冲突)
import { multiply as mul, age } from "./module.js";

2. 导入默认成员(Default Import)

// 导入默认函数,自定义名称为 subtract
import subtract from "./module.js";
// 导入默认类,自定义名称为 Person
import Person from "./module.js";

3. 混合导入(命名 + 默认)

若一个模块同时有命名导出和默认导出,可在一次import中同时导入:

// 格式:默认成员在前,命名成员在 {} 中

import defaultMember, { named1, named2 as n2 } from "./module.js";

4. 整体导入(Import All)

将模块所有导出成员(命名 + 默认)打包成一个对象,通过对象访问成员(默认成员会挂在default属性上):

import * as moduleAll from "./module.js";
// 使用:默认成员→moduleAll.default,命名成员→moduleAll.xxx
console.log(moduleAll.default); // 访问默认导出的成员
console.log(moduleAll.name);    // 访问命名导出的 name 变量
console.log(moduleAll.add(1,2));// 调用命名导出的 add 函数

三、关键注意事项

  1. 文件路径要求

    导入本地模块时,必须写全路径(相对路径需加 ./ 或 …/,绝对路径需符合规范),不能省略后缀(如 .js若用构建工具如 Webpack 可配置省略)。

    • 示例:import { fn } from “./utils.js”(正确),import { fn } from “utils”(仅对 npm 包有效)。
  2. 作用域限制

    import和export只能在模块顶层使用(不能在函数、if 语句等代码块中),因为模块解析是 “静态的”(在代码执行前完成)。

  3. 浏览器兼容性

    浏览器原生支持 ES6 模块,需在 HTML 的

  1. 与 CommonJS 的区别:

    • ES6 模块是 “静态的”(编译时确定依赖),CommonJS(如 Node.js 的require)是 “动态的”(运行时确定依赖)。
    • ES6 模块的import是 “只读引用”,CommonJS 的require是 “值拷贝”。

十二、按改变原数组和不改变原数据,介绍数组方法

数组方法可根据是否改变原数组分为两大类,核心区别在于方法执行后,原始数组的元素或结构是否被修改。

一、改变原数组的方法(会修改原始数组)

这类方法执行后,原数组的内容或长度会直接发生变化,无需重新赋值即可看到修改结果。

1. push()

​ 在数组末尾添加一个 / 多个元素,返回新长度。

​ 示例:let arr = [1,2]; arr.push(3); → 原数组变为 [1,2,3]。

2. pop()

​ 删除数组末尾的 1 个元素,返回被删除的元素。

​ 示例:let arr = [1,2,3]; arr.pop(); → 原数组变为 [1,2]。

3. unshift()

​ 在数组开头添加一个 / 多个元素,返回新长度。

​ 示例:let arr = [2,3]; arr.unshift(1); → 原数组变为 [1,2,3]。

4. shift()

​ 删除数组开头的 1 个元素,返回被删除的元素。

​ 示例:let arr = [1,2,3]; arr.shift(); → 原数组变为 [2,3]。

5. splice()

​ 多用途方法,可删除、插入、替换元素,返回被删除元素组成的数组。

​ 示例:let arr = [1,2,3]; arr.splice(1,1,4); → 原数组变为 [1,4,3](从索引 1 删 1 个元素,插入 4)。

6. sort()

​ 对数组元素排序(默认按字符串 Unicode 码排序),返回排序后的原数组。

​ 示例:let arr = [3,1,2]; arr.sort(); → 原数组变为 [1,2,3]。

7. reverse()

​ 反转数组元素顺序,返回反转后的原数组。

​ 示例:let arr = [1,2,3]; arr.reverse(); → 原数组变为 [3,2,1]。

二、不改变原数组的方法(原始数组保持不变)

这类方法执行后,会生成一个新数组 / 新值作为结果,原数组的内容和结构完全不受影响,需通过变量接收结果。

1. concat()

​ 合并两个 / 多个数组,返回合并后的新数组。

​ 示例:let arr1 = [1,2]; let arr2 = [3]; let newArr = arr1.concat(arr2); → 原数组不变,newArr 为 [1,2,3]。

2. slice()

​ 截取数组的指定片段(含头不含尾),返回截取的新数组。

​ 示例:let arr = [1,2,3,4]; let newArr = arr.slice(1,3); → 原数组不变,newArr 为 [2,3]。

3. map()

​ 遍历数组,对每个元素执行同一操作,返回操作后的新数组(长度与原数组一致)。

​ 示例:let arr = [1,2,3]; let newArr = arr.map(item => item*2); → 原数组不变,newArr 为 [2,4,6]。

4.filter()

​ 遍历数组,筛选出符合条件的元素,返回筛选后的新数组(长度可能小于原数组)。

​ 示例:let arr = [1,2,3,4]; let newArr = arr.filter(item => item%2===0); → 原数组不变,newArr 为 [2,4]。

5. reduce()

​ 遍历数组,对元素进行累积计算(如求和、求积),返回最终的累积值(非数组,需根据逻辑定义)。

​ 示例:let arr = [1,2,3]; let sum = arr.reduce((prev, curr) => prev + curr, 0); → 原数组不变,sum 为 6。

6. find()

​ 遍历数组,返回第一个符合条件的元素(无符合条件则返回undefined)。

​ 示例:let arr = [1,2,3]; let res = arr.find(item => item>2); → 原数组不变,res 为 3。

7. findIndex()

​ 遍历数组,返回第一个符合条件的元素的索引(无符合条件则返回-1)。

​ 示例:let arr = [1,2,3]; let idx = arr.findIndex(item => item>2); → 原数组不变,idx 为 2。

8. join()

​ 将数组元素按指定分隔符拼接成字符串,返回拼接后的字符串。

​ 示例:let arr = [1,2,3]; let str = arr.join(‘-’); → 原数组不变,str 为 “1-2-3”。

三、splice()方法的具体用法

splice() 是 JavaScript 数组中多用途且会改变原数组的方法,核心作用是对数组进行「删除、插入、替换」操作,返回值为被删除元素组成的新数组(若未删除则返回空数组)

一、语法结构

array.splice(start, deleteCount, item1, item2, …)

参数说明(前 2 个为核心参数,后为可选插入参数):

o 0 或负数:不删除任何元素(此时需搭配后面的「插入参数」实现插入功能)。

o 省略:默认删除从 start 到数组末尾的所有元素。

二、3 种核心用法(附示例)

假设初始数组:let arr = [1, 2, 3, 4, 5];

1. 仅删除元素(不插入)

场景:删除数组中指定位置、指定个数的元素。关键:deleteCount > 0,不传递「插入参数」。

示例 1:删除索引 2(第三个元素)开始的 1 个元素

let deleted = arr.splice(2, 1); 
// 原数组 arr 变为:[1, 2, 4, 5](删除了元素3)
// 返回值 deleted 为:[3](被删除的元素组成的数组)

**示例 2:**从末尾倒数第 2 个元素(索引 3)开始,删除所有元素

let deleted = arr.splice(-2); 
// 原数组 arr 变为:[1, 2](删除了4、5)
// 返回值 deleted 为:[4, 5]
2. 仅插入元素(不删除)

场景:在指定位置插入一个或多个元素,原数组元素后移。关键:deleteCount = 0,必须传递「插入参数」。

示例:在索引 1(第二个元素)处插入 6、7

let deleted = arr.splice(1, 0, 6, 7); 
// 原数组 arr 变为:[1, 6, 7, 2, 3, 4, 5](2及后面元素后移)
// 返回值 deleted 为:[](未删除元素,返回空数组)

3. 替换元素(先删后插)

场景:用新元素替换数组中指定位置的元素。关键:deleteCount > 0,且传递「插入参数」(删除 n 个元素,同时插入 m 个元素,n 和 m 可不等)。

示例 1:用 8 替换索引 2(第三个元素)的 1 个元素

let deleted = arr.splice(2, 1, 8); 
// 原数组 arr 变为:[1, 2, 8, 4, 5](删除3,插入8)
// 返回值 deleted 为:[3]

示例 2:用 9、10 替换索引 1 开始的 2 个元素(删除 2、3,插入 9、10)

let deleted = arr.splice(1, 2, 9, 10); 
// 原数组 arr 变为:[1, 9, 10, 4, 5]
// 返回值 deleted 为:[2, 3]

三、关键注意点

  1. 改变原数组:这是 splice() 与 slice()(不改变原数组)的核心区别,使用时需注意原数组是否需要保留。

  2. 返回值:始终返回「被删除元素的数组」,而非修改后的原数组。

  3. 索引越界:若 start 超出数组长度(如数组长度为 5,start=10),则插入 / 删除操作会在数组末尾进行。

四、 Some和every方法详解

some() 和 every() 是 JavaScript 数组的迭代方法,均不改变原数组,核心作用是通过回调函数判断数组元素是否满足特定条件,返回值均为 布尔值(true/false),但逻辑判断方向不同。

一、some () 方法:“存在即真”

some() 检测数组中是否至少有 1 个元素满足回调函数的条件,只要找到 1 个符合条件的元素,就立即返回 true;若所有元素都不满足,则返回 false(可理解为 “有一个算一个”)。

  1. 语法

array.some(callback(element, index, array), thisArg)

o element(必选):当前正在处理的数组元素。

o index(可选):当前元素的索引。

o array(可选):调用 some() 的原数组。

2. 示例

假设数组:const numbers = [1, 3, 5, 7];

示例 1:判断是否有偶数(结果为 false,因所有元素都是奇数)

const hasEven = numbers.some(num => num % 2 === 0);
console.log(hasEven); // false

示例 2:判断是否有元素大于 5(结果为 true,因 7 满足条件)

const hasGreaterThan5 = numbers.some(num => num > 5);
console.log(hasGreaterThan5); // true

二、every()方法:“缺一不可”

every() 检测数组中所有元素是否都满足回调函数的条件,只有所有元素都符合条件,才返回 true;若存在 1 个不满足的元素,立即返回 false(可理解为 “缺一不可”)。

1. 语法

与 some() 完全一致,仅逻辑判断不同:

array.every(callback(element, index, array), thisArg)

2. 示例

仍使用数组:const numbers = [1, 3, 5, 7];

示例 1:判断所有元素是否都是奇数(结果为 true,所有元素均满足)

const allOdd = numbers.every(num => num % 2 !== 0);
console.log(allOdd); // true

示例 2:判断所有元素是否都大于 5(结果为 false,因 1、3、5 不满足)

const allGreaterThan5 = numbers.every(num => num > 5);
console.log(allGreaterThan5); // false

三、核心区别与注意点

特性some()every()
判断逻辑至少 1 个满足 → true所有满足 → true
短路机制找到 1 个满足即停止迭代找到 1 个不满足即停止迭代
空数组调用时的返回值false(无元素可满足 “至少 1 个”)true(逻辑上 “所有元素都满足”,属于空真)

关键注意点:

  1. 不改变原数组:两者均为 “只读” 迭代,不会修改原数组内容。

  2. 短路迭代:一旦满足返回条件(some() 找到真、every() 找到假),会立即停止遍历,提升效率。

  3. 空数组特殊情况:空数组调用 some() 恒为 false,调用 every() 恒为 true(需注意业务场景中的边界处理)。

五、 Set 和map

Set 和 Map 是 JavaScript 中两种常用的引用类型数据结构,均用于存储数据,但核心用途、存储形式和特性有显著区别,且都不允许键(或值)重复(Set 是值不重复,Map 是键不重复),均不改变原数组 / 对象,常用于数据去重、关联存储等场景。

一、Set:无重复值的集合

Set 是值的集合,核心特点是 “存储唯一值”(无重复),值可以是任意类型(基本类型、引用类型),不按索引访问,更适合用于 “去重” 或 “判断值是否存在”。

1. 核心特性

2. 常用方法(增删查)

假设初始化 Set:const mySet = new Set();

方法作用示例结果
add(value)添加值,返回 Set 本身(可链式调用)mySet.add(1).add(2) .add(2);mySet 为 {1, 2}
delete(value)删除指定值,成功返回 true,否则 falsemySet.delete(1);返回 true,mySet 为 {2}
has(value)判断值是否存在,返回布尔值mySet.has(2);返回 true
clear()清空所有值,无返回值mySet.clear();mySet 为空 {}
size(属性,非方法)获取元素个数mySet.size;初始添加后为 2

3. 典型用途:数组去重

const arr = [1, 2, 2, 3, 3, 3];

const uniqueArr = […new Set(arr)]; // 利用 Set 去重后,通过扩展运算符转数组

console.log(uniqueArr); // [1, 2, 3]

二、Map:键值对的集合

Map 是 - 值对的集合,核心特点是 “键可以是任意类型”(基本类型、引用类型,如对象、数组),且键唯一(重复键会覆盖旧值),更适合用于 “关联存储”(如存储对象对应的附加信息),弥补了对象 “键只能是字符串 / Symbol” 的限制。

1. 核心特性

2. 常用方法(增删查)

假设初始化 Map:const myMap = new Map();(可传入二维数组初始化:new Map([[key1, value1], [key2, value2]]))

方法作用示例结果
set(key, value)添加键值对,返回 Map 本身(可链式调用)myMap.set(‘name’, ‘Alice’).set(18, ‘age’);myMap 含 {‘name’:‘Alice’, 18:‘age’}
get(key)获取指定键的值,无此键返回 undefinedmyMap.get(‘name’);返回 ‘Alice’
delete(key)删除指定键,成功返回 true,否则 falsemyMap.delete(18);返回 true,myMap 剩 {‘name’:‘Alice’}
has(key)判断键是否存在,返回布尔值myMap.has(‘name’);返回 true
clear()清空所有键值对,无返回值myMap.clear();myMap 为空 {}
size(属性,非方法)获取键值对个数myMap.size;初始添加后为 2

3. 典型用途:关联存储对象信息

const user1 = { id: 1 };

const user2 = { id: 2 };

const userRoles = new Map();

// 为不同对象关联“角色”值(键是对象,值是角色)

userRoles.set(user1, ‘admin’).set(user2, ‘user’);

console.log(userRoles.get(user1)); // 输出 ‘admin’(通过对象键获取对应值)

三、Set 与 Map 的核心区别

维度SetMap
存储形式仅存储 “值”(值的集合)存储 “键 - 值对”(映射关系)
核心用途数据去重、判断值是否存在关联存储(键值映射)、替代对象(非字符串键场景)
关键方法add()(添加值)、无 get()set()(添加键值对)、get()(按键取值)
迭代内容直接迭代 “值”迭代 “键值对”([key, value])

共同注意点:

  1. 不改变原数据:操作 Set/Map 时,不会影响初始化时传入的数组 / 对象。

  2. 引用类型的唯一性:若存储引用类型(如对象),即使内容相同,不同引用也视为不同的值 / 键(例如 new Set([{a:1}, {a:1}]) 会存储两个对象,因引用不同)。

  3. 迭代方式:均支持 for…of、forEach 遍历(Set 的 forEach 回调只有 “值” 参数,Map 有 “值、键、Map 本身” 三个参数)。

六、 数组去重

数组去重是指移除数组中重复的元素,保留唯一值。以下是 4 种常用且实用的去重方法,涵盖不同场景(ES6 / 兼容性 / 复杂数据),核心区别在于简洁度、兼容性和对特殊值(如 NaN)的处理。

一、ES6 Set 去重(推荐:简洁高效)

原理:利用 Set 数据结构 “值唯一” 的特性,先将数组转为 Set 自动去重,再转回数组。优点:1 行代码搞定,支持 NaN 去重(Set 中 NaN 视为与自身相等),不改变原数组。缺点:不支持 ES6 的环境(如旧版 IE)无法使用;无法去重引用类型元素(如 {a:1},因引用地址不同)。

const arr = [1, 2, 2, 3, NaN, NaN, '3'];
const uniqueArr = [...new Set(arr)]; // 扩展运算符转数组
// 或 const uniqueArr = Array.from(new Set(arr));
console.log(uniqueArr); // [1, 2, 3, NaN, "3"]

二、filter + indexOf 去重(兼容旧环境)

原理:filter 筛选元素,仅保留 “当前元素在数组中第一次出现的索引等于当前索引” 的元素(indexOf 返回元素第一次出现的索引)。优点:兼容性好(支持 ES5),不改变原数组。缺点:无法处理 NaN(因 indexOf(NaN) 始终返回 -1,会过滤掉所有 NaN);效率略低于 Set(需遍历 + 查找)。

const arr = [1, 2, 2, 3, '3'];
const uniqueArr = arr.filter((item, index) => {
  return arr.indexOf(item) === index; // 只保留第一次出现的元素
});
console.log(uniqueArr); // [1, 2, 3, "3"]
// 若数组含 NaN:会被全部过滤,结果为 [1, 2, 3, "3"]

三、对象键名去重(处理简单类型,避免 NaN 问题)

原理:利用 “对象键名唯一” 的特性,将数组元素作为对象的键名存储,避免重复;同时可解决 NaN 无法去重的问题(需特殊处理 NaN 的键名)。优点:支持 NaN 去重,兼容性好。缺点:需处理键名类型冲突(如 1 和 ‘1’ 会被视为同一个键,需拼接类型前缀);不支持引用类型。

const arr = [1, 2, 2, 3, NaN, NaN, '3'];
const obj = {};
const uniqueArr = [];
for (const item of arr) {
  // 处理 NaN(对象键名不能直接存 NaN,用特殊字符串代替)
  const key = item === NaN ? 'NaN_SPECIAL' : (typeof item + '_' + item);
  if (!obj[key]) {
    obj[key] = true;
    uniqueArr.push(item);
  }
}
console.log(uniqueArr); // [1, 2, 3, NaN, "3"]

四、Map 去重(灵活处理键类型)

原理:类似 Set,利用 Map 键名唯一的特性,将数组元素作为 Map 的键名存储,重复键会被覆盖,最后提取键名即为去重后的数组。优点:支持 NaN 去重,键名可存任意类型(理论上支持引用类型,但需手动判断引用地址,实际用得少)。缺点:ES6 特性,旧环境不支持;比 Set 代码略繁琐(Set 更专注 “值”,Map 适合 “键值对”)。

const arr = [1, 2, 2, 3, NaN, NaN];
const map = new Map();
const uniqueArr = [];
for (const item of arr) {
  if (!map.has(item)) { // 判断键是否已存在    map.set(item, true);
    uniqueArr.push(item);
  }
}
console.log(uniqueArr); // [1, 2, 3, NaN]

关键注意点

  1. 特殊值 NaN:Set 和 Map 能正确去重 NaN,indexOf 无法处理(会过滤所有 NaN),对象去重需特殊标记。
  2. 引用类型元素:上述方法均无法直接去重(如 [{a:1}, {a:1}]),需自定义比较逻辑(如通过 JSON.stringify(item) 转字符串比较,或比较具体属性)。
  3. 原数组影响:所有方法均不改变原数组(需通过变量接收结果)。

十二、 举例说明微任务和宏任务,以及执行顺序

微任务是Promise.then/catch/finally、async/await 等,优先级高;宏任务是 setTimeout**、setInterval、DOM 事件** 等,优先级低。执行顺序是:先执行同步代码,再清空所有微任务,最后执行一个宏任务,随后重复 “清空微任务→执行一个宏任务” 的循环。

// 1. 同步代码(先执行)
console.log('同步代码'); 
// 2. 宏任务(最后按顺序执行)
setTimeout(() => {
  console.log('宏任务1');
}, 0);
// 3. 微任务(同步代码后立即清空)
Promise.resolve().then(() => {
  console.log('微任务1');
});
// 4. 再一个宏任务
setTimeout(() => {
  console.log('宏任务2');
}, 0);

执行结果顺序:同步代码 → 微任务 1 → 宏任务 1 → 宏任务 2

十三、判断数据类型的方法 typeof和instanceof和Object.prototype.toString.call()的用法返回及区别

1. typeof:快速判断基本类型,引用类型判断有限

2. instanceof:判断引用类型的 “原型归属”

3. Object.prototype.toString.call ():最精准的 “万能判断法”

三者核心区别

维度typeofinstanceofObject.prototype.toString.call()
判断范围仅基本类型(除 null)+ function仅引用类型(原型链检测)所有数据类型(基本 + 引用)
返回值字符串(如 “number”)布尔值(true/false)固定格式字符串(如 “[object Array]”)
核心缺陷误判 null 和非 function 引用类型无法判断基本类型无明显缺陷(需完整调用)

4.区分null和undefined

区分null和undefined的核心是语义意图实际场景,具体可从 3 个关键维度划分:

1. 核心语义:“未定义” vs “主动清空”

2. 关键判断方式:精准识别

3. 实际使用场景:避免混淆

十四、Js中字符串方法

一、获取字符串信息

1.length

​ 获取字符串的长度(非方法,是属性)。

​ 示例:“hello”.length; // 结果:5

2.indexOf(searchValue, startIndex)

​ 从左向右查找指定字符 / 字符串的首次出现位置,找到返回索引,未找到返回 -1。

​ 示例:“abcabc”.indexOf(“a”, 1); // 结果:3

3.lastIndexOf(searchValue, startIndex)

​ 从右向左查找指定字符 / 字符串的最后出现位置,未找到返回 -1。

​ 示例:“abcabc”.lastIndexOf(“c”); // 结果:5

4.includes(searchValue, startIndex)

​ 判断字符串是否包含指定字符 / 字符串,返回布尔值(true/false)。

​ 示例:“hello world”.includes(“world”); // 结果:true

5.startsWith(searchValue, startIndex)

​ 判断字符串是否以指定字符 / 字符串开头,返回布尔值。

​ 示例:“hello”.startsWith(“he”); // 结果:true

6.endsWith(searchValue, length)

​ 判断字符串是否以指定字符 / 字符串结尾,返回布尔值。

​ 示例:“hello”.endsWith(“lo”); // 结果:true

二、修改字符串(返回新字符串,原字符串不变)

1.toUpperCase()

​ 将字符串全部转为大写

​ 示例:“hello”.toUpperCase(); // 结果:“HELLO”

2.toLowerCase()

​ 将字符串全部转为小写

​ 示例:“HELLO”.toLowerCase(); // 结果:“hello”

3.trim()

​ 去除字符串首尾的空格(不处理中间空格)。

​ 示例:" hello ".trim(); // 结果:“hello”

4.slice(startIndex, endIndex)

​ 从 startIndex 截取到 endIndex(不包含 endIndex),支持负数(表示从末尾倒数)。

​ 示例:“abcdef”.slice(1, 4); // 结果:“bcd”;“abcdef”.slice(-3); // 结果:“def”

5.substring(startIndex, endIndex)

​ 与 slice 类似,但不支持负数,且 startIndex 大于 endIndex 时会自动交换位置。

​ 示例:“abcdef”.substring(4, 1); // 结果:“bcd”

6.replace(searchValue, replaceValue)

​ 替换字符串中首个匹配的字符 / 正则表达式结果,若需全局替换需加正则 g 修饰符。

​ 示例:“aaa”.replace(“a”, “b”); // 结果:“baa”;“aaa”.replace(/a/g, “b”); // 结果:“bbb”

7.split(separator, limit)

​ 将字符串按 separator(分隔符)拆分为数组,limit 可选(限制数组长度)。

​ 示例:“a,b,c”.split(“,”); // 结果:[“a”, “b”, “c”];“a,b,c”.split(“,”, 2); // 结果:[“a”, “b”]

8.repeat(count)

​ 将字符串重复 count 次,count 为非负整数。

​ 示例:“ab”.repeat(3); // 结果:“ababab”

三、其他常用方法

1.charAt(index)

​ 获取指定索引位置的字符,索引越界返回空字符串。

​ 示例:“hello”.charAt(1); // 结果:“e”

2.charCodeAt(index)

​ 获取指定索引位置字符的Unicode 编码(十进制)。

​ 示例:“A”.charCodeAt(0); // 结果:65

3.fromCharCode(…codes)

​ 静态方法,需通过 String 调用)将 Unicode 编码转为字符

​ 示例:String.fromCharCode(65, 66); // 结果:“AB”

四、字符串转数组

在 JavaScript 中,将字符串转为数组最常用的方法是 split(),此外还有 Array.from() 和扩展运算符 […str],以下是具体用法:

1. split(separator, limit)(最常用)

2. Array.from(str)

3. 扩展运算符 […str]

五、数组转字符串

1. join(separator)

这是数组转字符串最常用的方法,能自定义元素之间的分隔符,返回拼接后的新字符串。

// 示例1:默认分隔符(逗号)
const arr1 = ["a", "b", "c"];
console.log(arr1.join()); // 输出:"a,b,c"
// 示例2:自定义分隔符(空格)
const arr2 = [1, 2, 3];
console.log(arr2.join(" ")); // 输出:"1 2 3"
// 示例3:无分隔符(空字符串)
const arr3 = ["hello", "world"];
console.log(arr3.join("")); // 输出:"helloworld"
// 示例4:特殊分隔符(横线)
const arr4 = ["2025", "12", "22"];
console.log(arr4.join("-")); // 输出:"2025-12-22"(常用作日期格式)

2. toString()(简单快捷,固定分隔符)

该方法会将数组转为字符串,元素之间固定用逗号分隔,无法自定义分隔符,适合只需要逗号分隔的简单场景。

const arr1 = [10, 20, 30];
console.log(arr1.toString()); // 输出:"10,20,30"
const arr2 = ["js", "html", "css"];
console.log(arr2.toString()); // 输出:"js,html,css"

3. 隐式转换(快捷写法)

利用 JavaScript 的隐式类型转换特性,将数组与空字符串 “” 拼接,本质上是自动调用数组的 toString() 方法,效果和 toString() 完全一致。

const arr = [true, false, null];
console.log(arr + ""); // 输出:"true,false,null"(等价于 arr.toString())

总结:

1. 优先用 join():需要自定义分隔符(如空格、横线、空字符)时,join() 是最优选择,灵活性最高。

2. 简单场景用 toString()/ 隐式转换:仅需逗号分隔时,toString() 或 arr + “” 写法更简洁。

3. 所有方法均不会修改原数组,而是返回新的字符串。

十五、深拷贝和浅拷贝

深拷贝指创建一个完全独立的新对象,新对象与原对象的所有层级数据(包括嵌套的子对象、数组等)都不共享内存,修改新对象不会影响原对象。

一、核心区别(与浅拷贝对比)

二、常见实现方式(以 JavaScript 为例)

1.简单但有局限性:JSON 序列化 / 反序列化

通过 JSON.stringify() 把对象转成字符串,再用 JSON.parse() 转回对象,快速实现深拷贝。优点:代码极简;缺点:无法拷贝函数、undefined、Symbol、循环引用对象。

const obj = { a: 1, b: { c: 2 } };
const deepCopyObj = JSON.parse(JSON.stringify(obj));
deepCopyObj.b.c = 3; // 原对象 obj.b.c 仍为 2(不影响)

2. 灵活可控:递归遍历拷贝

手动遍历对象的所有属性,遇到嵌套对象 / 数组时递归调用拷贝逻辑,支持所有数据类型(包括函数、循环引用)。

function deepClone(target, map = new Map()) {
  // 处理基本类型、null、函数(直接返回)
  if (typeof target !== 'object' || target === null) return target;
    // 处理循环引用(避免无限递归)
  if (map.has(target)) return map.get(target);
  const cloneTarget = Array.isArray(target) ? [] : {};
  map.set(target, cloneTarget); // 存储已拷贝的对象,解决循环引用
    // 遍历所有属性(包括 Symbol 键)
  Reflect.ownKeys(target).forEach(key => {
    cloneTarget[key] = deepClone(target[key], map); // 递归拷贝子属性
  });
   return cloneTarget;
}
// 测试:支持循环引用
const obj = { a: 1 };
obj.b = obj; // 循环引用(obj.b 指向自身)
const copy = deepClone(obj);
console.log(copy.b === copy); // true(拷贝成功,无报错)

3. 第三方库:成熟工具函数

开发中常用成熟库的深拷贝方法,避免重复造轮子,兼容性和性能更优。

**- Lodash**:_.cloneDeep(target)(最常用,支持所有数据类型,处理循环引用)
import _ from 'lodash';
const obj = { a: 1, b: { c: 2 } };
const copy = _.cloneDeep(obj);
**jQuery**:$.extend(true, {}, target)(第一个参数为 true 时开启深拷贝)
const obj = { a: 1, b: { c: 2 } };
const copy = $.extend(true, {}, obj);

4.解构赋值默认是浅拷贝,

仅对对象 / 数组的顶层数据进行复制,嵌套的子对象或子数组仍与原数据共享内存。

对于基本类型(如数字、字符串、布尔值):解构后是独立的新值,修改不会影响原数据。

const { a } = { a: 10 };
a = 20; // 原对象的 a 仍为 10(不影响)

对于嵌套的引用类型(如子对象、子数组):解构后仅复制引用,修改子数据会同时影响原对象和新对象。

const obj = { a: { b: 10 } };
const { a: newA } = obj;
newA.b = 20; // 原对象的 obj.a.b 也会变成 20(共享内存)

十六、通过不同方式,实现一个width:100px;height:100px;的盒子绝对居中

1. 绝对定位 + 负 margin(需知盒子宽高)

原理:先通过绝对定位让盒子左上角居中,再用负 margin 将盒子偏移自身一半尺寸,实现整体居中。

.parent {
  position: relative; /* 父容器需相对定位,作为定位参考 */
  width: 500px; 
  height: 500px;
}
.box {
  width: 100px;
  height: 100px;
  position: absolute;
  top: 50%; /* 垂直方向偏移父容器50% */
  left: 50%; /* 水平方向偏移父容器50% */
  margin-top: -50px; /* 垂直回移自身高度的一半(100px/2) */
  margin-left: -50px; /* 水平回移自身宽度的一半(100px/2) */
}

2. 绝对定位 + transform(无需知盒子宽高)

原理:用transform: translate(-50%, -50%)替代负 margin,自动计算盒子自身的偏移量,适配未知宽高场景。

.parent {
  position: relative;
  width: 500px;
  height: 500px;
}

.box {
  width: 100px;
  height: 100px;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%); /* 自身宽高的50%偏移 */
}

3. 绝对定位 + 四边 auto(需知盒子宽高)

原理:绝对定位的盒子,当top/bottom/left/right均设为 0 且margin: auto时,浏览器会自动分配空间,实现居中。

.parent {
  position: relative;
  width: 500px;
  height: 500px;
}
.box {
  width: 100px;
  height: 100px;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right:0;
  margin: auto; /* 关键:自动填充四边空白 */
}

4. Flex 布局(推荐,无需知盒子宽高)

原理:通过父容器的 Flex 属性,直接控制子元素在水平和垂直方向上居中,代码简洁且适配性强。

.parent {
  display: flex; /* 父容器设为Flex布局 */
  justify-content: center; /* 子元素水平居中 */
  align-items: center; /* 子元素垂直居中 */
  width: 500px;
  height: 500px;
}

.box {
  width: 100px;
  height: 100px;
}

5. Grid 布局(简洁,无需知盒子宽高)

原理:Grid 布局的place-items: center可同时实现水平和垂直居中,是目前最简洁的实现方式之一。

.parent {
  display: grid; /* 父容器设为Grid布局 */
  place-items: center; /* 等同于 justify-items: center + align-items: center */
  width: 500px;
  height: 500px;
}
.box {
  width: 100px;
  height: 100px;
}

十七、回调地狱及解决

一、回调地狱(Callback Hell)

回调地狱(Callback Hell)是指在 JavaScript 中,为了处理顺序执行的异步操作,将多个回调函数嵌套在一起,导致代码层级过深、可读性极差、维护困难的现象。

二、回调地狱的解决方法

核心思路是扁平化异步代码,主流方案有两种:

1.Promise 链式调用

将异步操作封装为 Promise,用 .then() 替代嵌套回调;

2.async/await**

基于 Promise 的语法糖,让异步代码看起来像同步代码(最推荐)。

三、Vue3 中的示例(结合实际开发场景)

以下示例基于 Vue3 的

方案 1:Promise 链式调用(解决回调地狱)

getUserInfo()
  .then((res) => {
    user.value = res;
    return getOrderByUserId(res.id); // 返回下一个Promise
  })
 .then((res) => {
    order.value = res;
    return getGoodsByOrderId(res.orderId); // 返回下一个Promise
 })
 .then((res) => {
    goods.value = res;

  })
  .catch((err) => { // 统一捕获所有异步错误
    console.error("请求出错:", err);
  })
  .finally(() => {
    loading.value = false;
  });

方案 2:async/await(最优解,代码最简洁)

const fetchData = async () => {
  try {
    // 用await等待每个异步操作完成,代码像同步一样顺序执行
    const userRes = await getUserInfo();
    user.value = userRes;
    const orderRes = await getOrderByUserId(userRes.id);
    order.value = orderRes;
    const goodsRes = await getGoodsByOrderId(orderRes.orderId);
    goods.value = goodsRes;
 } catch (err) {
    // 统一捕获所有异步错误
    console.error("请求出错:", err);
 } finally {
    loading.value = false;
 }
};

四、关键补充(Vue3 中使用 async/await 的注意点)

总结

  1. 回调地狱:多层嵌套的异步回调函数,导致代码可读性、可维护性极差;

  2. 核心解决思路:用 Promise 扁平化异步代码,优先使用 async/await 语法糖;

  3. Vue3 实践:将异步请求封装为 Promise,在

十八、有些场景下,为什么用promise不用axios

首先需要明确一个关键前提:Axios 本身基于 Promise 实现,二者并非 “二选一” 的替代关系,而是 “工具” 与 “底层规范” 的依赖关系。用 Promise 不用 Axios 的场景,本质是不需要 Axios 封装的额外能力,仅需 Promise 提供的异步控制能力。

核心场景主要有以下两类:

1. 非 HTTP 请求的异步操作

Axios 的核心功能是 “封装 HTTP 请求”,若异步逻辑与网络请求无关,仅需控制代码执行顺序(如定时器、文件读取、回调函数改造),则直接用 Promise 更轻量,无需引入 Axios 冗余代码。

// 仅用 Promise 控制异步顺序,无需 Axios
new Promise((resolve) => {
  setTimeout(() => resolve("延迟 1 秒后执行"), 1000);
}).then((res) => console.log(res));
// 用 Promise 包裹文件读取回调(Node.js 场景),无需 Axios
const fs = require("fs");
function readFilePromise(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, "utf8", (err, data) => {
	    err ? reject(err) : resolve(data);
    });
  });
}
// 链式调用,逻辑更清晰
readFilePromise("./test.txt").then(data => console.log(data));

2. 需要自定义 HTTP 请求逻辑,拒绝 Axios 封装

Axios 内置了请求拦截、响应拦截、错误统一处理、超时设置等 “开箱即用” 功能,但在某些场景下,这些封装反而会成为负担,需用原生 fetch(基于 Promise)或自定义 Promise 实现更灵活的请求控制:

// 用 fetch(基于 Promise)发送简单请求,无需 Axios
fetch("https://api.example.com/data")
  .then(res => res.json())
  .then(data => console.log(data));

到此这篇关于vue+js常见基础面试题及答案总结大全的文章就介绍到这了,更多相关vue+js基础面试题内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文