记 2025-02-27 裸辞,2025-03-21 收获 offer

前言

本人 2025-02-27 裸辞,2025-03-21 收获 offer。该文章记录了在面试过程中被提问到的问题,并进行总结记录。

面试题

Vue2.0 和 Vue3.0 有什么区别

1、响应式重新配置,使用 proxy 替换 Object.defineProperty

  • Object.defineProperty:劫持整个对象,然后进行深度遍历所有属性 ,给每个属性添加gettersetter,实现响应式

  • proxy : 劫持整个对象,但不用深度遍历所有属性 ,同样需要添加 gettersetterdeleteProperty,实现响应式

javascript 复制代码
new Proxy(data, {
  // 拦截读取属性值
  get (target, prop) {
      return Reflect.get(target, prop)
  },
  // 拦截设置属性值或添加新属性
  set (target, prop, value) {
      return Reflect.set(target, prop, value)
  },
  // 拦截删除属性
  deleteProperty (target, prop) {
      return Reflect.deleteProperty(target, prop)
  }
})

2、新增组合 API(Composition API),更好的逻辑重用和代码组织

3、v-if v-for的优先级

5、支持多个根节点(template中不需要唯一根节点,可以直接放文本或者同级标签)

6、打包体积优化 (任何一个函数,如ref、reavtived、computed等,仅仅在用到的时候才打包)tree shanking

7、编译阶段的不同

Vue.js 2.x

  • 通过标记静态节点,优化 diff 的过程

vue.js 3.x

  • 标记和提升所有的静态节点,diff的时候只需要对比动态节点内容
  • 静态提升(hoistStatic),当使用静态提升时,所有静态的节点都被提升到 render 方法之外。只会在应用启动的时候被创建一次,之后使用只需要应用提取的静态节点,随着每次的渲染被不停的复用。
  • patch flag, 在动态标签末尾加上相应的标记,只能带 patchFlag 的节点才被认为是动态的元素,会被追踪属性的修改,能快速的找到动态节点,而不用逐个逐层遍历,提高了虚拟dom diff的性能
  • 缓存事件处理函数cacheHandler,避免每次触发都要重新生成全新的function去更新之前的函数

8、生命周期变化

  • vue3.x 中可以继续使用 vue2.x 的生命周期钩子,但有俩个被更名;

    javascript 复制代码
    beforeDestroy 修改成 beforeUnmount
    destroyed 修改成 unmounted
  • vue3.x 生命周期钩子,与 vue2.x 中对应关系

    vue2.x vue3.x 解释
    beforeCreate setup() 数据观测和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到data、computed、watch、methods上的方法和数据
    created setup() 实例创建完成,实例上配置的 options 包括 data、computed、watch、methods 等都配置完成,但是此时渲染得节点还未挂载到 DOM,所以不能访问到 $el 属性
    beforeMount onBeforeMount 在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板 ,把data里面的数据和模板生成html。此时还没有挂载html到页面上
    mounted onMounted 用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html 页面中。此过程中进行ajax交互。
    beforeUpdate onBeforeUpdate 响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染
    updated onUpdated 发生在更新完成之后,当前阶段组件 DOM 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新,该钩子在服务器端渲染期间不被调用
    beforeUnmount onBeforeUnmount 实例销毁之前调用。这一步,实例仍然完全可用,this 仍能获取到实例
    unmounted onUnmounted 实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定 ,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用

组件的双向数据绑定

  1. vue3.4 之前
js 复制代码
<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>

根据上面的基本写法,同理对于自定义组件而言,我们的写法如下:

js 复制代码
<template>
  <objRange v-model="range" />
</template>
<script setup>
import { ref } from 'vue'
    
const range = ref([])    
</script>
js 复制代码
<!-- objRange -->
<template></template>
<script setup>
import { defineEmits, defineProps } from 'vue'
const props = defineProps({
  // v-model 默认绑定到 modelValue 属性
  modelValue: {
    type: Array,
    default: () => []
  }
})

// 定义事件抛出 update:xxx 中的 xxx 是对应绑定的属性
const emits = defineEmits(['update:modelValue'])

// 改变值
const changeValue = () => {
  const newValue = ['GRP-90843']
  // 将 update:xxx 事件抛出,实现数据双向绑定 
  emits('update:modelValue', newValue)
}
</script>
<style lang="scss" scoped></style>

v-model 默认是绑定到 modelvalue 属性上,我们也可以绑定到其他属性上,由此衍生这里可以衍生出多个属性的双向数据绑定,具体写法如下:

js 复制代码
<template>
  <objRange v-model:range="range" v-model:area="area" />
</template>
<script setup>
import { ref } from 'vue'
    
const range = ref([])    
const area = ref([])
</script>
js 复制代码
<!-- objRange -->
<template></template>
<script setup>
import { defineEmits, defineProps } from 'vue'
const props = defineProps({
  range: {
    type: Array,
    default: () => []
  },
  area: {
    type: Array,
    default: () => []
  }
})
// 将对应的 update:xxx 抛出即可
const emits = defineEmits(['update:range', 'update:area'])
</script>

Composition Api 与 Options Api 有什么不同

1、代码组织

  • Options Api 代码按照选项 (datamethodscomputedwatch)进行分组
  • Composition Api 代码按照逻辑功能进行分组

2、逻辑复用

  • Options Api 逻辑复用通常通过 mixins 来实现,但容易导致命名冲突和代码可读性下降。
  • Composition Api 逻辑复用通过自定义 Hook(类似于 React 的 Hooks)实现,可以将逻辑提取到独立的函数中,更灵活且易于维护。

3、this 的使用

  • Options Api 通过 this 访问组件实例的属性和方法
  • Composition API 在 setup 函数中没有 this,所有数据和函数都需要通过 return 暴露给模板

Vue中的$nextTick有什么作用

Vue 的响应式系统是异步的。

当数据发生变化时,Vue 并不会立即更新 DOM,而是将更新操作推入一个队列,并在下一个事件循环中批量处理。

意味着,如果在数据变化后立即访问 DOM,可能会获取到未更新的 DOM 状态。

$nextTick 提供了一种机制,确保在 DOM 更新完成后再执行代码。


keep-alive 有什么作用

1、keep-alive 是 vue 的内置组件,主要用来缓存动态组件路由组件的,避免组件在切换时被销毁和重新创建。

2、使用场景

  • 缓存路由组件

    vue 复制代码
    <template>
      <keep-alive>
        <router-view></router-view>
      </keep-alive>
    </template>
  • 缓存动态组件

    vue 复制代码
    <template>
      <keep-alive>
        <component :is="currentComponent"></component>
      </keep-alive>
    </template>

3、<keep-alive> 会触发两个额外的生命周期钩子

  • activated 当缓存的组件被激活时调用(即组件再次显示时)
  • deactivated 当缓存的组件被停用时调用(即组件被隐藏时)

4、<keep-alive> 支持以下属性

  • include:只有名称匹配的组件会被缓存。可以是字符串、正则表达式或数组
  • exclude:名称匹配的组件不会被缓存。可以是字符串、正则表达式或数组

5、缓存组件实例会占用内存,如果缓存过多组件,可能会导致内存占用过高。


为什么data属性是一个函数而不是一个对象

确保每个组件实例都有自己独立的数据副本,避免多个组件实例共享同一个数据对象,从而导致数据污染和状态混乱。


watch、computed的区别

  • computed 作用:是通过多个变量计算得出一个变量的值(多对一)。并且 computed有缓存的功能。当多个变量值,没有发生改变时,直接在缓存中读取该值。不支持异步操作。
  • watch 作用:侦听一个变量,从而影响其他变量(一对多)。支持异步操作。

Vue 列表为什么要加 key

Vue 使用虚拟 DOM 来优化渲染性能。当列表数据发生变化时,Vue 会通过对比新旧虚拟 DOM 来确定需要更新的部分。如果没有 key,Vue 会默认使用"就地复用"策略,即尽可能复用相同类型的元素,而不是重新创建或移动它们。


MVVM是什么?和MVC有何区别呢?

  • Model(模型):负责从数据库中取数据
  • View(视图):负责展示数据的地方
  • Controller(控制器):用户交互的地方,例如点击事件等等
  • VM: 视图模型

在 MVVM 中,View 不知道 Model 的存在,Model 和 ViewModel 也观察不到 View,这种低耦合模式提高代码的可重用性。VM 会自动将数据更新到页面中,而 MVC 需要手动操作 dom 将数据进行更新


ref、unref、isRef 、toRef、toRefs、toRaw 区别

javascript 复制代码
// 定义响应式变量
const name1 = ref('name1') 
// 普通变量
const name2 = 'name2'
// reactive 定义响应式变量
const obj = reactive({ name: 'name3' })

// isRef 是判断变量是否为 ref
console.log(isRef(name1), isRef(name2), isRef(obj)) // true false false

// unref 如果是 ref 返回其内部的值,反之返回参数本身
console.log(unref(name1), unref(name2), unref(obj)) // name1 name2 { name: 'name3' }(参数本身)

// toRef 针对响应式数据的单一属性
const name3 = toref(obj, 'name')
// 此时修改 name3;会影响到 obj.name
// 同理修改 obj.name;也会影响到 name3

// toRefs 针对响应式数据的所有属性
// 若使用下述代码,解构出来的属性是没有响应式的
const { name4: name } = obj
// 正确的解构应该是
const { name5: name } = toRefs(obj)
// 此时修改 name5;会影响到 obj.name
// 同理修改 obj.name;也会影响到 name5

// toRefs 也可以用于解构 prop,确保解构出来的属性有响应式
const {} = prop


// toRaw 可以返回 reactive、readonly、shallowReactive 创建的代理所对应的原始对象
const original = { count: 0 }
const reactiveData = reactive(original)

const rawData = toRaw(reactiveData) // 获取原始对象

rawData.count += 10 // ❌ 修改原始对象,不会触发更新

isProxy 、isReactive、isReadOnly 区别

(很少用到)

  • isProxy:检查对象是否是由reactive或readonly创建的代理。
  • isReactive:检查对象是否是reactive创建的,或者被包裹在一个readonly中的原始reactive代理。
  • isReadonly:检查对象是否是readonly创建的代理。
方法 作用 典型返回值场景
isProxy 检测对象是否是 任意代理对象 (由 reactivereadonly 创建) reactive(obj)true readonly(obj)true 普通对象false
isReactive 检测对象是否是 响应式代理 (由 reactive 创建或被 readonly 包裹的响应式对象) reactive(obj)true readonly(reactive(obj))true readonly(obj)false
isReadonly 检测对象是否是 只读代理 (由 readonly 创建) readonly(obj)true reactive(obj)false

验证代码

js 复制代码
<template>
  <div>
    <p>原始对象: {{ rawObject }}</p>
    <p>响应式对象: {{ reactiveObj }}</p>
    <p>只读对象: {{ readonlyObj }}</p>
    <p>只读包裹响应式对象: {{ readonlyReactiveObj }}</p>
  </div>
</template>

<script setup>
import { reactive, readonly, isProxy, isReactive, isReadonly } from 'vue'

// 原始对象
const rawObject = { name: 'Alice' }

// 响应式对象
const reactiveObj = reactive(rawObject)

// 只读对象(直接包裹原始对象)
const readonlyObj = readonly(rawObject)

// 只读包裹响应式对象
const readonlyReactiveObj = readonly(reactive({ age: 25 }))

// 检测函数
const check = (obj, name) => {
  console.log(`----- ${name} -----`)
  console.log('isProxy:', isProxy(obj))
  console.log('isReactive:', isReactive(obj))
  console.log('isReadonly:', isReadonly(obj))
}

// 执行检测
check(rawObject, '原始对象')          // 全部返回 false
check(reactiveObj, '响应式对象')       // isProxy: true, isReactive: true, isReadonly: false
check(readonlyObj, '只读对象')         // isProxy: true, isReactive: false, isReadonly: true
check(readonlyReactiveObj, '只读包裹响应式对象') 
// isProxy: true, isReactive: true, isReadonly: true
</script>

ref、 shallowRef、reactive、shallowReactive 区别

ref shallowRef
refValue.value.count++ // 触发更新 shallowRefValue.value.count++ // 不触发更新 shallowRefValue.value = newObj // 触发更新
内部值会被深度代理,修改嵌套属性会触发响应式更新 仅监听 .value 的引用变化,不会深度代理内部属性
reactive shallowReactive
reactiveObj.nested.count++ // 触发更新 shallowReactiveObj.nested.count++ // 不触发更新 shallowReactiveObj.nested = { count: 100 }
递归代理所有层级的属性,嵌套对象也会响应式 只代理对象的第一层属性,嵌套对象保持原始状态

验证代码

js 复制代码
<template>
  <div>
    <h3>ref vs shallowRef</h3>
    <p>ref: {{ refValue.count }}</p>
    <p>shallowRef: {{ shallowRefValue.count }}</p>
    <button @click="changeRefInner">修改 ref 内部属性</button>
    <button @click="changeShallowRefInner">修改 shallowRef 内部属性</button>
    <button @click="changeShallowRefValue">替换 shallowRef 整个值</button>

    <h3>reactive vs shallowReactive</h3>
    <p>reactive.nested: {{ reactiveObj.nested.count }}</p>
    <p>shallowReactive.nested: {{ shallowReactiveObj.nested.count }}</p>
    <button @click="changeReactiveNested">修改 reactive 嵌套属性</button>
    <button @click="changeShallowReactiveNested">修改 shallowReactive 嵌套属性</button>
    <button @click="changeShallowReactiveValue">替换 shallowReactive 整个值</button>

  </div>
</template>

<script setup>
import { ref, shallowRef, reactive, shallowReactive } from 'vue'

// ----------------------
// 1. ref vs shallowRef
// ----------------------
const refValue = ref({ count: 0 }) // 深层响应式
const shallowRefValue = shallowRef({ count: 0 }) // 仅监听 .value 变化

const changeRefInner = () => {
  refValue.value.count++ // 触发更新
}

const changeShallowRefInner = () => {
  shallowRefValue.value.count++ // ❌ 不会触发更新
}

const changeShallowRefValue = () => {
  shallowRefValue.value = { count: 100 } // ✅ 触发更新
}

// ----------------------
// 2. reactive vs shallowReactive
// ----------------------
const reactiveObj = reactive({
  nested: { count: 0 } // 深层响应式
})

const shallowReactiveObj = shallowReactive({
  nested: { count: 0 } // 仅顶层响应式
})

const changeReactiveNested = () => {
  reactiveObj.nested.count++ // ✅ 触发更新
}

const changeShallowReactiveNested = () => {
  shallowReactiveObj.nested.count++ // ❌ 不会触发更新
}
const changeShallowReactiveValue = () => {
  shallowReactiveObj.nested = { count: 1 } // ✅ 触发更新
}
</script>

defineProps 参数有哪些

js 复制代码
<template></template>
<script setup>
defineProps({
    theme: {
        type: String,
        default: 'dark',
        required: true,
        validator: (value) => {
            return ['dark', 'light'].includes(value)
        }
    }
})
</script>

Suspense 是如何使用的

js 复制代码
<template>
  <Suspense>
    <!-- 默认插槽:显示异步组件 -->
    <template #default>
      <AsyncComponent />
    </template>

    <!-- fallback 插槽:加载中显示的内容 -->
    <template #fallback>
      <div>加载中...</div>
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from 'vue'

// 定义一个异步组件
const AsyncComponent = defineAsyncComponent(() =>
  import('./AsyncComponent.vue')
)
</script>

v-slotted 选择器如何使用

js 复制代码
<template>
  <div class="child-component">
    <!-- 定义插槽 -->
    <slot></slot>
  </div>
</template>

<style scoped>
.child-component {
  border: 1px solid #ccc;
  padding: 10px;
}

/* 选择插槽内带有.container 类的元素 */
::v-slotted(.container) {
  background-color: lightyellow;
  border: 1px solid #ffcc00;
  padding: 15px;
}
</style>
js 复制代码
<template>
  <div>
    <!-- 使用子组件并向插槽传递内容 -->
    <ChildComponent>
      <div class="container">
        <p>这是插槽内.container 里的内容</p>
      </div>
      <p>这是插槽内普通的内容</p>
    </ChildComponent>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
  components: {
    ChildComponent
  }
}
</script>

pina 和 vuex 在使用上有什么区别

  • pina 使用上更为简洁,基于 composition API;而 vuex 是基于 options API;
  • pina 天然模块化,每一个 store 都是独立的;而 vuex 需要手动划分;
  • pina 对 TS 的支持更为友好;vuex 需要额外配置
  • pina 体积更小;vuex 体积稍大
  • pina 允许直接修改状态,更为灵活;vue 需要通过 mutations 修改状态,更为严格

localStorage 、cookie、sessionStorage 三者的区别

  • 存储大小:Cookie 4k;Storage 5M;
  • 有效期:Cookie 拥有有效期;localStorage 永久存储;sessionStorage 会话存储
  • Cookie 会发送到服务器端,存储在内存中;Storage 只会存储在浏览器端
  • 路径:Cookie 有路径限制,Storage 只存储在域名下
  • API:Cookie 没有特定的 API;Storage 有对应的 API;

数组去重方法

javascript 复制代码
// 方法一
const arr1 = [...new Set(originalArr)]

// 方法二(缺点 无法过滤 NaN) [NaN].indexOf(NaN) = -1
const arr2 = originalArr.fillter((item, index) => originalArr.indexof(item) === index)

// 方法三
const arr3 = originalArr.reduce((acc, cur) => acc.includes(cur) ? acc : [...acc, cur], [])

对象拷贝方法

javascript 复制代码
// 浅拷贝

// 方法一 扩展运算符
const obj = { ... originalObj }

// 方法二 Object.assign
const obj = Object.assign({}, originalObj)

// 方法三 for in 
for (let key in originalObj) {
    if (originalObj.hasOwnProperty(key)) {
        obj[key] = originalObj[key]
    }
}

// 深拷贝

// 方法一:缺点 无法拷贝函数
const obj = JSON.parse(JSON.stringify(originalObj))

// 方法二 递归
function deepClone(originalObj) {
    if (obj === null || typeof originalObj != 'object') return originalObj
    
    const clone = Array.isArray(originalObj) ? [] : {}
    
    for(let key in originalObj) {
        if (originalObj.hasOwnProperty(key)) {
            clone[key] = deepClone(originalObj[key])
        }
    }
    
    return clone
}

数组交集、并集、差集

js 复制代码
let arr2 = [1, 2, 3, 4, 5]
let arr3 = [3, 4, 1, 2]

// 交集
console.log(arr2.filter(item => arr3.includes(item)))

// 并集
console.log(Array.from(new Set([...arr2, ...arr3])))

// arr2 差集
console.log(arr3.filter(item => !arr2.includes(item)))

// arr3 差集
console.log(arr2.filter((item) => !arr3.includes(item)))

数组扁平

js 复制代码
function flatter(arr) {
  if (!arr.length) return;
  
  return arr.reduce((pre, cur) => {
    return Array.isArray(cur) ? [...pre, ...flatter(cur)] : [...pre, cur]
  }, []);
}

// 测试
let arr = [1, 2, [1, [2, 3, [4, 5, [6]]]]]
console.log(flatter(arr));

CSS 如何实现水平垂直方向居中

css 复制代码
/* 方法一 flex 布局 */ 
.container {
    display: flex;
    justify-content: center;
    align-items: center;
}


/* 方法二 绝对定位 + transform */ 
.container {
    position: relative;
}

.child {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, 50%)
}


/* 方法三 绝对定位 + margin */
.container {
    position: relative;
}

.child {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    margin: auto;
}

/* 方法四 表格布局 */
.container {
    disaply: table-cell;
    vertical-align: middle;
    text-align: center;
}

.child {
    display: inline-block;
}

讲一下 let 和 const

提出了 块级作用域 概念

1、什么是块级作用域:

  • 在该作用域外无法访问该变量

2、块级作用域存在于:

  • 函数内部
  • 块中(字符 { 和 } 之间的区域)

3、let 和 const 特性

  • 变量不会被提升

    javascript 复制代码
    if (false) {
        let value = 1
    }
    console.log(value); // Uncaught ReferenceError: value is not defined
  • 重复声明该变量会报错

  • 不会绑定到全局作用域上

4、临时性死区(TDZ)

let 和 const 声明的变量不会被提升到作用域顶部,如果在声明之前访问这些变量,会导致报错

javascript 复制代码
console.log(typeof value); // Uncaught ReferenceError: value is not defined
let value = 1;

介绍一下箭头函数

  • 箭头函数没有 this 指向,需要通过作用域来确定 this 的值

    this 绑定的就是最近一层非箭头函数的 this

    由于没有 this,因此 call,apply,bind 不能被使用

    三者的区别:

    • 三者都可以绑定函数的 this 指向
    • 三者第一个参数都是 this 要指向的对象,若该参数为 undefined 或 null,this则默认指向全局
    • 传参不同:apply 是数组;call 是参数列表,而 bind 可以分多次传入,实现参数合并
    • call apply 是立即执行,bind 是返回绑定 this 之后的函数,如果这个新的函数作为构造函数被调用,那么 this 不再指向传入给 bind 的第一个参数,而是指向新生成的对象
  • 箭头函数没有 arguments 对象

  • 不能通过 new 关键字进行调用

  • 没有原型

    javascript 复制代码
    var Foo = () => {};
    console.log(Foo.prototype); // undefined

如何遍历对象

可以查看另外一篇文章:# 细究 ES6 中多种遍历对象键名方式的区别

for...of for...in的区别如下

  • for...of 遍历获取的是对象的键值,for...in 获取的是对象的键名;
  • for... in 会遍历对象的整个原型链,性能非常差不推荐使用,而 for ... of 只遍历当前对象不会遍历原型链;
  • 对于数组的遍历,for...in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for...of 只返回数组的下标对应的属性值;

总结

for...in 循环主要是为了遍历对象而生,不适用于遍历数组;

for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。

相关推荐
最懒的菜鸟6 分钟前
spring boot jwt生成token
java·前端·spring boot
天天扭码17 分钟前
基于原生JavaScript实现H5滑屏音乐播放器开发详解
前端·css·html
Carlos_sam17 分钟前
canvas学习:如何绘制带孔洞的多边形
前端·javascript·canvas
文岂_17 分钟前
不可解的Dom泄漏问题,Dom泄漏比你预期得更严重和普遍
前端·javascript
本地跑没问题18 分钟前
HashRouter和BrowserRouter对比
前端·javascript·react.js
很酷爱学习18 分钟前
ES6 Promise怎么理解?用法?使用场景?
前端·javascript
76756047918 分钟前
深入剖析 JavaScript 中的 `Number.isNaN` 和 `isNaN`:区别与应用场景
前端
忆柒19 分钟前
Vue自定义指令:从入门到实战应用
前端·javascript·vue.js
海底火旺19 分钟前
Bootstrap 响应式布局实战指南
前端·bootstrap
酷酷的阿云20 分钟前
解放双手!Vue3全局方法与指令自动导入最佳实践
前端·vue.js·vite