整理了一些基础的前端面试题,后续会慢慢补充

0 阅读12分钟

弹窗组件

组件结构➡️插槽➡️样式➡️显示和隐藏的逻辑➡️事件处理

子组件:
<template>
    <div v-if="visible" class="modal-overlay">
        <div class="modal-container">
            <div class="modal-header">
                <h3>{{ title }}</h3>
                <button @click="close" class="close-btn">&times;</button>
            </div>
            <div class="modal-body">
                <slot></slot>
            </div>
            <div class="modal-footer">
                <button @click="close" class="cancel-btn">取消</button>
                <button @click="confirm" class="confimr-btn">确定</button>
            </div>
        </div>
    </div>
</template>

父组件:
 <template>
    <div>
        <button @click="showModal = true" class="open-modal-btn">打开弹窗</button>
        <Modal v-model:visible="showModal" title="自定义标题" @confirm="handleConfirm" @cancel="handleCancel">
            <h3>通过插槽传入的内容</h3>
            <p>可以在这放置任何自定义内容</p>
        </Modal>
    </div>
</template>

首页白屏

1.资源路径错误

打开vite.config.ts

export default defineConfig({
    pluginls: [vue()],
    // 判断是生产环境还是开发环境,使用不同的资源路径格式
    base: process.env.NODE_ENV === 'production' ? './' : '/'
})

2.路由配置错误

确保路由组件使用动态导入(import())并正确处理错误

const routes = [
    {
        path: '/',
        // 要配置一个error错误页面,资源没加载出来的时候指向error 404 页面
        component: () => import('@/views/Home.vue').catch(() => import('@/views/Error.vue')),
    },
 ];

排查流程:

打开开发者工具,检查Console和Network面板,定位错误类型

验证配置一致性

检查环境变量,确保生产环境变量正确定义并注入

后端接口返回的超大树形结构数据

1.虚拟滚动(最佳方案)

vue3的插件,只渲染当前可见的部分

<!-- :items 要渲染的节点数组 -->
<!-- :item-size 固定每个节点的高度为 32px,用于计算滚动条位置和渲染区域 -->
<!-- key-field 指定节点对象的唯一标识字段为 id -->
<!-- v-slot 定义作用域插槽,接收当前渲染的节点数据,并解构出 item 对象供内部使用 -->
<RecycleScroller
    class="scroller"
    :items="visibleNodes" 
    :item-size="32"
    key-field="id"
    v-slot="{ item }"
>
    <!-- 动态计算左内边距,根据节点的层级 level 实现缩进效果(每级缩进 20px) -->
    <!-- 点击节点时调用 toggleExpand 方法,传入当前节点,用于展开/折叠子节点 -->
    <div
        class="tree-node"
        :style="{ paddingLeft: `${item.level * 20}px` }"
        @click="toggleExpand(item)"
    >
        <!-- 只有当节点存在子节点(item.children 为真)时才显示展开/折叠图标 -->
        <!-- 根据节点的 expanded 状态显示不同图标:展开时显示 '-',折叠时显示 '+' -->
        <span v-if="item.children" class="expand-icon">
            {{ item.expanded ? '-' : '+' }}
        </span>
        {{ item.label }}
    </div>
</RecycleScroller>

2.懒加载

只加载当前展开节点的子节点,减少初始数据量

// 定义一个异步函数 loadChildren,用于加载树节点的子节点,并切换展开/折叠状态
const loadChildren = async (node: TreeNode) => {
    // 检查当前节点是否尚未加载子节点:如果没有 children 属性且未标记为已加载(loaded 为 false)
    if (!node.children && !node.loaded) {
        // 发起异步请求,获取该节点的子节点数据,URL 中携带当前节点的 id
        const response = await fetch(`/api/tree/children?id=${node.id}`)
        // 将响应的 JSON 数据解析并赋值给节点的 children 属性
        node.children = await response.json()
        // 标记该节点已加载过数据,避免重复请求
        node.loaded = true
    }
    // 切换节点的展开/折叠状态:如果原来是展开则折叠,原来是折叠则展开
    node.expanded = !node.expanded
}

3.数据分片处理(需要后端接口支持)

// 定义每个数据块的大小,即每次请求加载的节点数量
const chunkSize = 1000
// 记录当前已加载的块索引(从0开始),用于分页请求
let currentChunk = 0
// 创建一个响应式引用 loadedData,初始值为空数组,用于存储所有已加载的树节点
const loadedData = ref < TreeNode[] > ([])
// 定义一个异步函数,用于加载下一个数据块并更新 loadedData
const loadNextChunk = async () => {
    // 向服务器发起 GET 请求,获取指定块索引和大小的节点数据
    const response = await fetch(`/api/tree?chunk=${currentChunk}&size=${chunkSize}`)
    // 解析响应的 JSON 数据,得到当前块的节点数组
    const chunk = await response.json()
    // 将新加载的节点数组合并到现有的 loadedData 数组中(使用展开运算符创建新数组)
    loadedData.value = [...loadedData.value, ...chunk]
    // 块索引加1,准备加载下一个块
    currentChunk++
}

4.Web Worker...

浏览器类型

检查是移动端还是PC端

1.userAgent

function detectDevice() {
    // 使用JavaScript的userAgent方法检测用户设备
    const userAgent = navigator.userAgent.toLowerCase()
    // 用正则判断是什么设备
    const isMobile = /iphone|ipad|ipod|android|mobile|phone/i.test(userAgent)
    return isMobile ? 'mobile' : 'pc'
}

2.屏幕尺寸检测

userAgent检测不准确或检测不出来的备选方案

function detectDevice() {
    // 使用JavaScript的userAgent方法检测用户设备
    const userAgent = navigator.userAgent.toLowerCase()
    // 用正则判断是什么设备
    const isMobile = /iphone|ipad|ipod|android|mobile|phone/i.test(userAgent)
    const isSmallScreen = window.innerWidt < 768 // 768px 常见的移动端分界点
    return (isMobile || isSmallScreen) ? 'mobile' : 'pc'
}
扩展:延迟重定向(避免影响SEO)
setTimeout(() => {
    const isMobile = /iphone|ipad|ipod|android|mobile|phone/i.test(navigator.userAgent)
    window.location.href = isMobile ? '/h5-app' : '/web-app'
}, 100)

安全:防止其他人调试前端代码

核心防御技术

高级代码混淆:Obfuscator 开源工具

动态Debugger陷阱

setInterval(() => {
    // 记录当前高精度时间(毫秒)
    const start = performance.now()
    // 触发断点:如果开发者工具打开,脚本会在此暂停
    debugger
    // 记录暂停结束后的时间
    const end = performance.now()
    // 如果暂停时间超过100毫秒(说明debugger生效了)
    if (end - start > 100)
        // 立即跳转到空白页,使页面无法继续调试
        window.location.href = 'about:blank'
    // 每50毫秒执行一次检测
}, 50)

开发者工具检测

通过window.outerWidth - window.innerWidth 或 window.outerHeight - window.innerHeight判断是否打开了调试工具(差值通常 > 160px)

快捷键禁用 右键菜单禁用...

进阶防御策略

代码分片和动态加载

将关键代码拆分为多个片段,通过异步请求或import()动态加载,减少静态分析可能

反调试库集成

工具:disable-devtool 或 console-ban

使用JavaScript实现轻量级本地存储方案,用于保存和读取用户偏好设置

当localStorage存放超过5m容量时,会自动寻找时间久且使用频率不高的存储进行清除

// 存储前缀,避免命名冲突
prefix: 'app_prefs_'

// 存
function setLocal (key, value) {
    try {
        // 将值转换成字符串
        const serializedValue = JSON.stringify(value)
        // 值大小限制在5m内
        localStorage.setItem(this.prefix + key, serializedValue)
    } catch (error) {
        console.error('保存偏好设置失败:', error)
    }
}

// 取
function getLocal (key, defaultValue = null) {
    try {
        // 读取本地存储的值
        const serializedValue = localStorage.getItem(this.prefix + key)
        // 如果没读取到则返回null
        if (serializedValue === null) return defaultValue
        // 将值转换成对象 然后返回
        return JSON.parse(serializedValue)
    } catch (error) {
        console.error('读取偏好设置失败:' + error)
        return defaultValue
    }
}

// 删
function removeLocal () {
    // 获取 localStorage 中所有键的数组,并遍历每个键
    Object.keys(localStorage).forEach(key => {
        // 检查当前键是否以当前对象的 prefix 属性开头
        if (key.startsWith(this.prefix)) {
            localStorage.removeItem(key)
        }
    })
}

判断一个点是否在Canvas的图形内

function isPointInRect(x, y, rect) {
    return x >= rect.x && x <= rect.x + rect.width &&
           y >= rect.y && y <= rect.y + rect.height
}
 
const rect = { x: 10, y: 10, width: 100, height: 50 }
console.log(isPointInRect(30, 30, rect)) // true
console.log(isPointInRect(150, 30, rect)) // false

防止按钮重复提交

// 禁用提交按钮   如果提交时网络断开,按钮会一直禁用 需要刷新等手动处理
form.addEventListener('submit', (e) => {
    e.preventDefault()
    submitBtn.disabled = true
    submitBtn.textContent = '提交中...'
    setTimeout(() => {
        console.log('提交成功')
        submitBtn.textContent = '提交成功'
    }, 2000)
})

// 前端存储提交记录
form.addEventListener('submit', (e) => {
    e.preventDefault()
    
    // 临时存储一个提交了的值
    const hasSubmitted = sessionStorage.getItem('formSubmitted')
    
    if(hasSubmitted) {
        alert('请勿重复提交')
        return
    }
    
    sessionStorage.setItem('formSubmitted', 'true')
    
    console.log('表单提交')
})

与其他岗位的沟通

统一接口规范和文档

制定标准:与后端团队约定统一的接口规范(如 RESTful、GraphQL),明确请求/响应格式(如 JSON 结构、状态码定义)。

文档先行:使用工具(如 Swagger、YAPI、Apifox)维护实时更新的接口文档,要求后端在开发前提供清晰的接口定义,包括:

  • 请求方法(GET/POST等)
  • 路径(URL)
  • 参数(Query/Body/Header)及示例
  • 响应数据结构及示例
  • 错误码说明

版本控制:对接口文档进行版本管理,避免因接口变更导致前后端冲突。

接口环境隔离与配置

在Vue项目中使用 .env 文件区分不同环境(开发 测试 预发布等)

使用 mockjs 或 axios-mock-adapter 模拟后端接口

接口联调阶段划分

  • 并行开发:前端基于 Mock 数据开发界面,后端实现接口逻辑。

  • 初步联调:后端接口完成后,前端切换到真实环境测试,记录问题并反馈。

  • 回归测试:后端修复问题后,前端再次验证接口。

  • 定期沟通...

项目从0到1搭建

  1. 使用 Vite 创建项目

    1. Assets 静态资源
    2. Components 全局可复用组件
    3. Composables 组合式函数
    4. Router 路由配置
    5. Stores 状态管理
    6. Styles 全局样式
    7. Utils 工具函数(路由守卫)
    8. Views 页面级组件
  2. 安装必要插件

    1. vue-router
    2. Element-plus
    3. Axios
  3. 项目配置

    1.  export default defineConfig({
           plugins: [vue()],
           resolve: {
               alias: {
                   '@': path.resolve(__dirname, './src') // 用 @ 代替 ./src
               }
           },
           server: {
               port: 3000, // 指定端口号
               open: true // 服务器启动后,自动在默认浏览器中打开应用程序
           },
           build: {
               outDir: 'dist',
               sourcemap: true
           }
       })
      
  4. 路由配置

    1.  import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
       const routes: Array<RouteRecordRaw> = [
           {
               path: '/',
               name: 'Home',
               component: () => import(@/views/HomeView.vue)
           },
           {
               path: '/about',
               name: 'About',
               component: () => import(@/views/AboutView.vue)
           }
       ]
       const router = createRouter({
           history: createWebHistory(import.meta.env.BASE_URL),
           routes
       })
      
       export default router
      
  5. Pinia 状态管理

    1.  import { defineStore } from 'pinia'
      
       export const useCounterStore = defineStore('counter', {
           state: () => ({
               count: 0
           }),
           actions: {
               increment() {
                   this.count++
               }
           }
       })
      
  6. 主入口文件

    1.  import { createApp } from 'vue'
       import App from './App.vue'
       import router from './router'
       import { createPinia } from 'pinia'
       import { ElementPlus } from 'element-plus'
       import 'element-plus/dist/index.css'
      
       const app = createApp(App)
      
       app.use(createPinia())
       app.use(router)
       app.mount('#app')
      

多并发处理

  • 请求合并与批量处理
 // 定义一个异步函数 fetchMultipleData
// 接收一个 apis 参数
// apis 是一个包含多个接口配置的对象数组
async function fetchMultipleData(apis) {
    // 使用 try...catch 捕获可能出现的错误
    try {
        // 使用 map 遍历 apis 数组,对每个接口调用 fetch 函数,返回一个 Promise 数组
        // fetch 的第二个参数是请求配置
        // 这里使用了 api 对象中的 method 属性(如 'GET'、'POST' 等)
        const requests = apis.map(api => fetch(api.url, { method: api.method }))
        // 使用 Promise.all 等待所有 fetch 请求完成
        // 返回一个包含所有 Response 对象的数组
        const responses = await Promise.all(requests)
        // 再次使用 Promise.all 等待所有响应的 json() 方法解析完成
        // 返回一个包含所有解析后数据的数组
        // 注意:responses.map(res => res.json()) 会生成一组 Promise
        // 所以需要用 Promise.all 等待
        return await Promise.all(responses.map(res => res.json()))
    } catch (error) {
        console.log(error)
        throw error
    }
}

import { useData } from './composables/useData';
export default {
    setup() {
        const { data, loading, error } = useData(fetchMultipleData([
            { url: '/api/users', method: 'GET' },
            { url: '/api/products', method: 'GET' }
        ]));
        return { data, loading, error };
    }
}

防抖节流

防抖

在用户停止输入一段时间后才触发请求,避免频繁请求,适用于搜索输入框

<!-- lodash的debounce -->
<template>
    <input v-model="keyword" @input="handleInput" />
</template>

<script setup>
    import { ref } from 'vue'
    import { debounce } from 'lodash'
    const keyword = ref('')
    // 使用 debounce 包装搜索函数
    const search = debounce(() => {
        console.log('搜索关键词:', keyword.value)
    }, 500)
    const handleInput = () => {
        search()
    }
    // 组件卸载时自动取消(如果使用 setup 语法糖,需在 onUnmounted 中处理)
    import { onUnmounted } from 'vue'
    onUnmounted(() => {
        search.cancel()
    })
</script>


<!-- 手写防抖 -->
<script>
const handleInput = (e) => {
    clearTimeout(timer)
    timer = setTimeout(() => {
        const query = e.target.value.trim()
        if(query) {
            console.log('发送搜索请求:', query)
            // 实际API的调用
            // fetch(`/api/search?q=${query}`)
        }
    }, 500)
}
</script>

节流

在固定时间间隔内最多触发一次请求,适用于滚动加载

<!-- lodash的throttle -->
<template>
    <div @scroll="handleScroll">...</div>
</template>

<script setup>
    import { throttle } from 'lodash'
    import { onUnmounted } from 'vue'
    const onScroll = () => {
        console.log('滚动触发')
    }
    const throttledScroll = throttle(onScroll, 200)
    const handleScroll = () => {
        throttledScroll()
    }
    onUnmounted(() => {
        throttledScroll.cancel()
    })
</script>

<!-- 手写节流 -->
<script>

let lastTime = 0

const handleInput = (e) => {
    const now = Date.now()
    if(now - lastTime < 500) return
    lastTime = now
    const query = e.target.value.trim()
    if(query) {
        console.log('发送搜索请求:', query)
        // 实际API的调用
        // fetch(`/api/search?q=${query}`)
    }
}
</script>

Vue列表组件的key的作用

  1. 高效更新虚拟 DOM
  2. 保持组件状态(勾选框等)
  3. 避免渲染错误

正确使用key:

v-for ➡️ 避免使用数组索引作为key,除非列表是静态而且不会重新排序

['1', '2', '3'].map(parseInt)

// 输出:
// [1, NaN, NaN]

// map有三个参数 当前值 下标 原数组 parseInt只能接收两个参数也就是 map 的前两个参数

// string是需要转换成数字的字符串 radix是代表需要转成多少进制  2~36
// parseInt(string, radix)

// 第一轮遍历
parseInt('1', 0) // radix为0时默认是十进制 返回 1
// 第二轮遍历
parseInt('2', 1) // radix为1 要求在2~36 返回NaN
// 第三轮遍历
parseInt('3', 2) // radix为2,二进制,但 3 不在有效范围内 二进制有效数字只能是0和1 返回NaN
// 最后返回 [1, NaN, NaN]

setTimeout Promise Async/Await 的区别

// setTimeout:宏任务
// 延时输出,延时时间自定义
// 无法直接处理异步操作的结果或错误
setTimeout(() => {
    console.log(1)
}, 500)

// Promise:微任务
// 封装异步操作,提供链式调用和状态管理

// Async/Await
// 基于Promise的语法糖,用同步风格编写异步代码

Async Await如何通过同步的方式实现异步

npm安装机制,为什么输入npm i 可以自动安装对应模块

Object.prototype.toString.call() instance of Array.isArray()

// Object.prototype.toString.call()
// 通用性强,但不够简洁,还需要额外处理返回值
const arr = [1, 2, 3]
console.log(Object.prototype.toString.call(arr)) // "[object, Array]"

// instanceof
// 写法简单,性能比Object.prototype.toString.call()更快
const arr = [1, 2, 3]
console.log(arr instanceof Array) // true

// Array.isArray()
// 写法简单,JS官方推荐
const arr = [1, 2, 3]
console.log(Array.isArray(arr))

回流和重绘 优化

重绘:元素的外观属性改变,不影响布局

回流:元素的几何属性(宽高/边距/位置等)或内容(文本/图片尺寸)改变,一定会触发重绘

优化方案:

  • 批量修改 DOM

  • 避免频繁读取布局属性

  • 防抖节流

const和let的变量存储位置

存储在当前的词法环境中,是JavaScript引擎内部的数据结构

Vue3的双向数据绑定 底层如何实现互相改变

reactive() ref() 使用Proxy包装对象,拦截所有读写的操作

访问响应式数据时,触发 Proxy 的 get 拦截器

get 拦截器会调用 track() 函数,收集依赖

修改响应式数据时,触发 Proxy 的 set 拦截器

set 拦截器调用 trigger() 函数,重新渲染组件

<input v-model="state.count" />
<!-- = -->
<input :value="state.count" @input="state.count = $event.target.value" />

在Vue中,子组件为什么不能修改父组件传过来的数据

  • 单向数据流:

    • 可预测性:数据变化唯一来源是父组件,便于调试
    • 可维护性:避免子组件意外修改导致数据混乱

可以通过事件通知父组件修改:

// 子组件
props: ['value'],
methods: {
    updateValue() {
        this.$emit('input', 'new value')
    }
}

// 父组件
<ChildComponent :value="parentValue" @input="parentValue = $event" />

HTTPS握手过程

暂时无法在飞书文档外展示此内容

apply()和call() 区别

function introduce(city, age) {
    console.log(this.name + ' 在' + city + ',年龄' + age)
}
const user = { name: '小明' }
// 一个一个传
introduce.call(user, '北京', 20) // 小明 在北京,年龄20
// 数组传
introduce.apply(user, ['上海', 22]) // 小明 在上海,年龄22

// call 通常更快
// apply需要将数组参数结构为独立参数,而call直接传递参数
// 需要动态生成参数列表时可以直接使用apply

// ES6 的优化方案
// 使用展开运算符可以让call接受数组参数,同时保持性能优势

Vue 中的 Object.defineProperty 有什么缺陷

  • 无法自动监听对象新增或删除的属性

    • 对于已经初始化的对象,通过直接赋值新增属性(比如:obj.newData = value)或者删除(delete obj.a),Object.defineProperty无法触发响应式更新
  • 对数组的监听不完整

    • 通过索引修改数组元素(arr[0] = newValue)或者修改数组长度(arr.length = newLength)都无法触发更新
  • 深度监听性能消耗大

    • 对嵌套对象进行响应式处理时,需递归遍历所有属性并逐个定义getter/setter,若对象层级深或数据量大,初始化阶段会消耗大量性能。(原因:Object.defineProperty 递归遍历是同步且强制的,无论属性是否被访问了,都需要提前劫持)

三种元素隐藏方式

  • Opacity: 0(需要保留交互或动画)

    • 视觉隐藏:透明不可见,但保留布局
    • 交互性:仍然可以响应点击,悬停等事件
    • 渲染:元素会被渲染到页面上,占用GPU资源
  • Visibility: hidden(需要保留布局但隐藏内容)

    • 视觉隐藏:不可见,但保留布局
    • 交互性:不响应任何事件
    • 渲染:元素会被渲染,但浏览器不绘制内容
  • Display: none(需要彻底移除布局而且没有交互)

    • 视觉隐藏:不可见,不保留布局,后续元素会填补位置
    • 交互性:不响应任何事件(完全从 DOM 中移除渲染)
    • 渲染:元素不会被渲染,不占用GPU资源