一天三场面试,口干舌燥要晕倒(二)

紧接着上篇文章。在结束了下午三点半的面试之后,六点迎来了我今天的第三场面试。此时我已经身体乏力、疲惫不堪了,没想到面试是这么消耗精力的一件事,需要集中精神全神贯注,说话方式也得和平时不一样,需要口齿伶俐,说白了就是得夹着嗓子说话(苦笑)。但好在这场面试答得比较好,第二天就给了offer。

好了废话不多说,一起来看看这份面试题吧。

1. vue3 的响应式原理

vue3的响应式原理主要是基于ES6打造的proxy来实现的。比如最常用的两个api,reactive 和 ref:

reactive 是借助proxy的代理作用,代理该引用类型的属性,当该属性被读取值时,返回该属性的值并且为该属性添加副作用函数;当该属性被修改值时,触发掉该属性的副作用函数

ref既可以代理原始类型也可以代理引用类型,当代理的是原始类型时,返回的是一个RefImpl的实例,通过类身上的get和set属性去读取值和修改值;当代理的是引用类型时,其实走的还是reactive的调用机制。

2. 聊聊虚拟dom

虚拟DOM是一个轻量级的JavaScript对象,是真实DOM的抽象表示,主要用于提升页面渲染性能,减少操作真实DOM结构的次数。因为操作真实DOM代价是非常昂贵的,会频繁引发回流与重绘。而采用虚拟DOM结构,当数据变化时,框架会生成一个新的虚拟DOM树,使用Diff算法将新旧两棵虚拟DOM树进行对比,找出需要更新的最小差异部分,将差异部分批量应用到真实DOM,减少渲染次数,并且让开发者只需要关注数据状态,无需手动操作DOM,大大地提升了开发效率,并且虚拟DOM还支持跨平台开发。

3. vue的两种路由方式

vue的路由方式有两种:hash路由和history路由,我已经写过文章了,详情请见这篇文章:

[]((笔记)前端路由1. 什么是前端路由 前端路由是通过url路径来匹配对应的代码块的这么一套机制 修改 url 后页面要更 - 掘金)

4. 浏览器的同源策略及跨域

浏览器的同源策略及跨域也是常考的问题之一了,JYM一定要在心里自己多复述几遍,确保能够清晰流利地讲给面试官听。面试是一个展示自己的过程,自己会的一定要大声流利地说出来,慢一点也没有关系,要让面试官清晰地知道我们是会的,让面试官给我们加分。

关于跨域的文章我也写过,在这里就不赘述了。跨域就请见这篇文章:

[> ]((笔记)什么是跨域?1. 什么是跨域? 想象一下,你住在一个小区里,小区有门禁系统,只允许本小区的住户自由进出。如果外来 - 掘金)

5. 浏览器的存储

浏览器的存储有四种,分别是:localStorage,sessionStorage、cookies和indexDB。它们的特点分别是:

localStorage是永久存储,大小在5MB-10MB左右,以字符串类型的键值对进行存储,不可跨域。

sessionStorage是本会话页面有效,大小也在5MB-10MB左右,以字符串类型的键值对进行存储,不可跨域。

cookies可以存放复杂的数据类型,大小4kb-5kb左右,只能由后端来设置,它在每次发送请求时都会自动携带在响应头中传给后端。

indexDB是一种客户端数据库,也能存放复杂数据类型,大小理论上无限大,根据你电脑的硬盘决定。

6. 浏览器的垃圾回收机制

这道题没有答出来,在那里胡编乱造(笑。

浏览器的垃圾回收机制(Garbage Collection, GC)是自动管理内存的核心机制,用于释放不再使用的对象所占用的内存,防止内存泄漏。以下是其核心原理和实现方式的详细说明:

1. 垃圾回收的基本目标

  • 自动释放内存 :开发者无需手动管理内存(如 malloc/free),由引擎自动追踪和回收"不可达"(Unreachable)的对象。
  • 避免内存泄漏:防止因程序逻辑错误导致无用对象长期占用内存。

2. 关键算法

2.1 标记-清除(Mark and Sweep)
  • 现代浏览器的主流算法(如 V8、SpiderMonkey)。

  • 步骤

    1. 标记阶段:从根对象(Roots,如全局变量、当前函数作用域链、活动线程等)出发,递归遍历所有可达对象,标记为"存活"。
    2. 清除阶段:遍历堆内存,回收未被标记的对象,将其内存返回空闲列表。
  • 优点:解决循环引用问题(若两个对象互相引用,但无法从根访问,仍会被回收)。

2.2 引用计数(Reference Counting)
  • 早期算法(如旧版 IE),因无法处理循环引用已基本被淘汰。
  • 原理:记录每个对象的被引用次数,当引用数为 0 时立即回收。
  • 缺陷 :循环引用会导致对象永远无法回收(如 A → B → A)。

3. 优化策略

3.1 分代回收(Generational Collection)
  • 内存分代:基于对象存活时间,将堆分为:

    • 新生代(Young Generation) :存放短生命周期对象(如临时变量)。使用 Scavenge 算法(复制存活对象到新空间,清空旧空间)。
    • 老生代(Old Generation) :存放长生命周期对象(如全局变量)。使用 标记-清除标记-整理(Mark-Compact) (清除后整理内存碎片)。
  • 晋升机制:新生代对象经历多次 GC 后仍存活,会被移到老生代。

3.2 增量标记(Incremental Marking)
  • 问题:一次性标记所有对象会导致主线程卡顿(Stop-The-World)。
  • 解决:将标记过程拆分为多个小步骤,穿插在 JavaScript 执行间隙。
3.3 惰性清理(Lazy Sweeping)
  • 清理阶段延迟执行或分片执行,减少对主线程的影响。
3.4 并行/并发回收
  • 并行:GC 任务在多个辅助线程并行执行(如 V8 的并行标记)。
  • 并发:GC 线程与主线程同时运行(需处理读写竞争)。

4. 触发 GC 的时机

  • 主动触发 :开发者可通过 window.gc()(需启动 Chrome 时加 --js-flags="--expose-gc")。

  • 被动触发

    • 分配内存时,空闲内存不足。
    • 脚本执行暂停期间(如事件循环空闲)。
    • 根据时间或内存分配阈值动态调整。

5. 内存泄漏的常见原因

即使有 GC,仍需开发者注意:

  1. 意外的全局变量 (未声明的变量会挂载到 window)。
  2. 未清除的定时器或事件监听 (如 setIntervaladdEventListener)。
  3. 闭包引用外部变量(如函数内部持有外部大对象)。
  4. 脱离 DOM 的引用 (如 const elements = document.querySelectorAll('div'),即使 DOM 移除,elements 仍引用节点)。

7. 组件传值

一、基础传值方式

1. Props / 自定义事件(父子通信)
  • Props(父 → 子) :父组件通过属性向子组件传递数据。
js 复制代码
<!-- 父组件 -->
<Child :title="parentTitle" />

<!-- 子组件 -->
<script setup>
const props = defineProps({
  title: { type: String, required: true }
});
</script>

自定义事件(子 → 父) :子组件通过 emit 触发事件,父组件监听。

js 复制代码
<!-- 子组件 -->
<button @click="$emit('update', newValue)">提交</button>

<!-- 父组件 -->
<Child @update="handleUpdate" />
2. v-model 双向绑定
1. 默认的单个 v-model
js 复制代码
<!-- 父组件 -->
<Child v-model="message" />

<!-- 等价于 -->
<Child 
  :modelValue="message" 
  @update:modelValue="newValue => message = newValue" 
/>

子组件需要定义 modelValue prop 并触发 update:modelValue 事件:

js 复制代码
<!-- 子组件 -->
<script setup>
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);

const handleInput = (e) => {
  emit('update:modelValue', e.target.value); // 将新值传给父组件
};
</script>

<template>
  <input 
    :value="modelValue" 
    @input="handleInput" 
  />
</template>
2. 多个 v-model 绑定

Vue3 允许同时绑定多个属性,例如同时绑定 nameage

js 复制代码
<!-- 父组件 -->
<Child v-model:name="userName" v-model:age="userAge" />

<!-- 等价于 -->
<Child 
  :name="userName" 
  @update:name="newName => userName = newName" 
  :age="userAge" 
  @update:age="newAge => userAge = newAge" 
/>

子组件需定义对应的 props 和事件:

js 复制代码
<!-- 子组件 -->
<script setup>
const props = defineProps(['name', 'age']);
const emit = defineEmits(['update:name', 'update:age']);

const updateName = (val) => {
  emit('update:name', val); // 触发 name 更新
};

const updateAge = (val) => {
  emit('update:age', val); // 触发 age 更新
};
</script>

二、跨层级通信

3. Provide / Inject
  • 父组件提供数据 :使用 provide 共享数据。
js 复制代码
// 父组件(Composition API)
import { provide, ref } from 'vue';

const count = ref(0);
provide('countKey', count); // 提供响应式数据

子孙组件注入数据 :使用 inject 获取数据。

js 复制代码
// 子组件
import { inject } from 'vue';

const injectedCount = inject('countKey', defaultValue);
4. 状态管理(Pinia / Vuex)
  • Pinia(推荐) :Vue3 官方推荐的状态管理库。
js 复制代码
// store/user.js
export const useUserStore = defineStore('user', {
  state: () => ({ name: 'Alice' }),
  actions: {
    updateName(newName) { this.name = newName; }
  }
});

// 组件中使用
const userStore = useUserStore();
userStore.updateName('Bob');

8. get 和 post 请求的区别

1. 设计目的

  • GET
    用于获取资源(幂等操作),不会修改服务器数据,仅请求指定信息。
  • POST
    用于提交数据到服务器(非幂等操作),通常会导致服务器状态变化(如创建或更新资源)。

2. 参数位置

  • GET
    参数通过**URL查询字符串(Query String)**传递,格式为 ?key1=value1&key2=value2,直接可见。
    示例:
    https://api.example.com/users?id=123&name=John
  • POST
    参数通过**请求体(Request Body)**传递,对用户不可见(但抓包仍可查看)。
    示例:
js 复制代码
POST /users HTTP/1.1
Content-Type: application/json
{"name": "John", "age": 30}

3. 数据长度限制

  • GET
    URL长度限制 (不同浏览器限制不同,通常为 2KB~8KB)。
    原因: 参数附加在URL中,浏览器或服务器可能截断超长URL。
  • POST
    理论上无长度限制(数据在请求体中),但实际受服务器配置限制(如Nginx默认限制为1MB)。

4. 缓存与历史记录

  • GET
    会被浏览器主动缓存 (如静态资源),URL可能保留在浏览器历史记录或书签中。
    场景: 重复访问同一URL时可直接使用缓存。
  • POST
    默认不缓存,浏览器不会保存请求体数据,也不会保留在历史记录中。

5. 安全性

  • GET
    参数明文暴露在URL中,可能被浏览器历史、服务器日志记录或他人直接看到,不适合传输敏感信息(如密码)。
  • POST
    参数在请求体中,相对更隐蔽,但若不使用HTTPS,数据仍可能被截获(如抓包工具)。
    注意: 安全性取决于是否加密(HTTPS),而非请求方法本身。

9. v-if 和 v-show 的区别

v-if直接让dom结构消失,不加载,而v-show是让display属性为none。

如果需要频繁的切换组件最好使用v-show。

10. rem 和 js实现移动端的适配是怎么做的?

使用js获取屏幕的宽度然后去修改根元素的字体daxiao,这样就能实现rem单位基于屏幕宽度的变化,1rem就是一倍根元素的字体大小。

js 复制代码
(function () {
  const setRootFontSize = () => {
    const designWidth = 750; // 设计稿宽度(如 750px 对应 iPhone 6/7/8)
    const baseFontSize = 100; // 1rem = 100px(方便计算,如设计稿中 200px → 2rem)
    const scale = document.documentElement.clientWidth / designWidth;// 获取屏幕宽度相对于设计稿屏幕宽度的倍数
    document.documentElement.style.fontSize = baseFontSize * Math.min(scale, 2) + 'px'; // 限制最大缩放比例
  };
  setRootFontSize();
  window.addEventListener('resize', setRootFontSize);
})();

11. css 的盒子模型

浏览器渲染一个容器时,会把容器渲染成 4 个区域,分别是 content、padding、border、margin。由这4 个区域组成了一个盒子模型。

标准盒模型width/height = content,不含 paddingborder

IE盒模型width/height = content + padding + border

切换box-sizing: content-box(标准) / border-box(IE模型)。

12. vue的slot

1. 默认插槽(Default Slot)

  • 作用 :父组件传递的内容会被渲染到子组件中 <slot> 标签的位置。
  • 子组件
js 复制代码
<!-- ChildComponent.vue -->
<template>
  <div>
    <slot>默认内容(当父组件未提供时显示)</slot>
  </div>
</template>

父组件

js 复制代码
<ChildComponent>
  <p>这是父组件传递的内容</p>
</ChildComponent>

2. 具名插槽(Named Slots)

  • 作用:通过名称标识多个插槽,实现内容精准分发。
  • 子组件
js 复制代码
<!-- ChildComponent.vue -->
<template>
  <div>
    <slot name="header"></slot>
    <slot></slot> <!-- 默认插槽 -->
    <slot name="footer"></slot>
  </div>
</template>

父组件

js 复制代码
<ChildComponent>
  <template v-slot:header>
    <h1>标题</h1>
  </template>
  
  <p>默认插槽的内容</p> <!-- 隐式对应默认插槽 -->
  
  <template #footer> <!-- 简写语法 -->
    <p>页脚</p>
  </template>
</ChildComponent>

3. 作用域插槽(Scoped Slots)

  • 作用:子组件向父组件的插槽传递数据,实现数据驱动的内容渲染。
  • 子组件
js 复制代码
<!-- ChildComponent.vue -->
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item"></slot>
    </li>
  </ul>
</template>

父组件

js 复制代码
<ChildComponent>
  <template v-slot:default="{ item }"> <!-- 接收子组件传递的数据 -->
    <span>{{ item.text }}</span>
  </template>
</ChildComponent>

13. 手写节流或深拷贝

最后一道题是手写题,从节流和深拷贝中选一个,我选了节流。

不知道为什么在面试的时候敲代码手都是抖的,思路也比较混乱,但还好是写出来的。

节流函数

节流函数的原理是:利用闭包存放上一次点击按钮时的时间,比较当前点击按钮的时间和上一次点击按钮的时间,如果时间小于设定的时间的话,不执行函数;当大于设定的时间时,才执行函数,并更新上次点击按钮的时间的值。

js 复制代码
function throttle(fn, wait) { 
    let preTime = null 
    return function (...args) { 
        let nowTime = Date.now() 
        if (nowTime - preTime > wait) { 
            fn.call(this, ...args) 
            preTime = nowTime 
} } }

深拷贝

深拷贝就是浅拷贝加上递归,当需要对象的属性是原始类型时,直接放到新对象中;当属性是引用类型时,递归自己:

js 复制代码
function deepClone(obj) {
  let clone = obj instanceof Array ? [] : {}
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {  
      if (obj[key] instanceof Object) {
        clone[key] = deepClone(obj[key])
      } else {
        clone[key] = obj[key]
      }
    }
  }
  return clone
}

总结

感觉知识永远学不完,看文章时总是有不会的,唉,革命尚未完成,同志还须努力。还得沉淀沉淀再沉淀!

相关推荐
Fantasywt3 小时前
THREEJS 片元着色器实现更自然的呼吸灯效果
前端·javascript·着色器
IT、木易4 小时前
大白话JavaScript实现一个函数,将字符串中的每个单词首字母大写。
开发语言·前端·javascript·ecmascript
Mr.NickJJ5 小时前
JavaScript系列06-深入理解 JavaScript 事件系统:从原生事件到 React 合成事件
开发语言·javascript·react.js
张拭心6 小时前
2024 总结,我的停滞与觉醒
android·前端
念九_ysl6 小时前
深入解析Vue3单文件组件:原理、场景与实战
前端·javascript·vue.js
Jenna的海糖6 小时前
vue3如何配置环境和打包
前端·javascript·vue.js
uhakadotcom6 小时前
Apache CXF 中的拒绝服务漏洞 CVE-2025-23184 详解
后端·面试·github
uhakadotcom6 小时前
CVE-2025-25012:Kibana 原型污染漏洞解析与防护
后端·面试·github
uhakadotcom6 小时前
揭秘ESP32芯片的隐藏命令:潜在安全风险
后端·面试·github
uhakadotcom6 小时前
Apache Camel 漏洞 CVE-2025-27636 详解与修复
后端·面试·github