Reader

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

| 掘金本周最热 | Default

前言

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

1.png

面试题

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

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

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

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

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 的生命周期钩子,但有俩个被更名;

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

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

组件的双向数据绑定

  1. vue3.4 之前
<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>

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

<template>
  <objRange v-model="range" />
</template>
<script setup>
import { ref } from 'vue'
    
const range = ref([])    
</script>
<!-- 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 属性上,我们也可以绑定到其他属性上,由此衍生这里可以衍生出多个属性的双向数据绑定,具体写法如下:

<template>
  <objRange v-model:range="range" v-model:area="area" />
</template>
<script setup>
import { ref } from 'vue'
    
const range = ref([])    
const area = ref([])
</script>
<!-- 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、使用场景

  • 缓存路由组件

    <template>
      <keep-alive>
        <router-view></router-view>
      </keep-alive>
    </template>
    
  • 缓存动态组件

    <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 区别

// 定义响应式变量
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

验证代码

<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 区别

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

验证代码

<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 参数有哪些

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

Suspense 是如何使用的

<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 选择器如何使用

<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>
<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;

数组去重方法

// 方法一
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], [])

对象拷贝方法

// 浅拷贝

// 方法一 扩展运算符
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
}

数组交集、并集、差集

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)))

数组扁平

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 如何实现水平垂直方向居中

/* 方法一 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 特性

  • 变量不会被提升

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

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

4、临时性死区(TDZ)

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

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 关键字进行调用

  • 没有原型

    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 对象。