2025最新面试自用题库---面试使用

2025最新面试自用题库---面试使用

一、2025最新面试题(真实反馈)

以下是整理后的面试题及参考回答,去除重复问题并按顺序排列,内容结合前端核心知识与实践经验:

以下是整理后(去除重复问题)的面试题及参考解答,按顺序呈现:

1. 生命周期(以Vue为例)

Vue组件的生命周期是组件从创建到销毁的整个过程,主要分为4个阶段:

  • 创建阶段beforeCreate(实例初始化后,数据观测前)→ created(实例创建完成,可访问数据和方法,未挂载DOM)。
  • 挂载阶段beforeMount(模板编译完成,未挂载到DOM)→ mounted(DOM挂载完成,可操作DOM)。
  • 更新阶段beforeUpdate(数据更新后,DOM未更新前)→ updated(DOM更新完成)。
  • 销毁阶段beforeDestroy(实例销毁前,仍可访问数据和方法)→ destroyed(实例销毁,事件监听和子组件均被移除)。

2. 小程序生命周期

小程序生命周期分为应用级页面级

  • 应用生命周期app.js):
    • onLaunch:小程序初始化完成时触发(全局只触发一次)。
    • onShow:小程序启动或从后台切换到前台时触发。
    • onHide:小程序从前台切换到后台时触发。
    • onError:小程序发生错误时触发。
  • 页面生命周期(页面.js):
    • onLoad:页面加载时触发(接收页面参数,只执行一次)。
    • onShow:页面显示时触发(每次打开页面都会执行)。
    • onReady:页面初次渲染完成时触发(可操作DOM)。
    • onHide:页面隐藏时触发(如跳转到其他页面)。
    • onUnload:页面卸载时触发(如返回上一页)。

3. 路由守卫(以Vue Router为例)

路由守卫用于控制路由跳转的权限和行为,分为3类:

  • 全局守卫
    • router.beforeEach:跳转前触发(常用作登录验证、权限控制)。
    • router.afterEach:跳转后触发(如修改页面标题)。
    • router.beforeResolve:在所有组件内守卫和异步路由组件解析后触发。
  • 路由独享守卫:在路由配置中定义beforeEnter,只作用于当前路由。
  • 组件内守卫
    • beforeRouteEnter:进入组件前触发(此时组件实例未创建,通过next(vm => {})访问实例)。
    • beforeRouteUpdate:组件复用(路由参数变化)时触发。
    • beforeRouteLeave:离开组件时触发(如提示未保存的修改)。

4. 小程序不同页面传参

  • URL参数传递:跳转时在url后拼接参数(如/pages/detail?id=1&name=test),目标页面通过onLoadoptions接收。
  • 本地存储:用wx.setStorageSync存储数据,目标页面用wx.getStorageSync读取(适合复杂数据,但需注意清理)。
  • 全局变量:在app.jsglobalData中定义,页面通过getApp().globalData访问(适合全局共享数据)。
  • 事件总线:通过wx.navigateToevents参数监听跨页面事件(如wx.navigateTo({ url: 'xxx', events: { onData: (data) => {} } }))。

5. 数组方法

  • 遍历类
    • forEach:遍历数组,无返回值(无法中断)。
    • map:遍历数组,返回新数组(每个元素为回调返回值)。
    • filter:筛选符合条件的元素,返回新数组。
    • find/findIndex:返回第一个符合条件的元素/索引,找到后中断。
  • 操作类
    • push/pop:尾部添加/删除元素,改变原数组。
    • shift/unshift:头部删除/添加元素,改变原数组。
    • splice:删除/添加/替换元素(参数:起始索引、删除数量、添加元素),改变原数组。
    • slice:截取子数组(参数:起始索引、结束索引),不改变原数组。
  • 其他reduce(累加器)、some(是否有符合条件的元素)、every(是否所有元素符合条件)等。

6. 执行顺序题(示例分析)

假设题目为:console.log(1); setTimeout(() => console.log(2), 0); new Promise(resolve => { console.log(3); resolve(); }).then(() => console.log(4)); console.log(5);

  • 执行顺序:1(同步)→ 3(Promise构造函数同步)→ 5(同步)→ 4(微任务)→ 2(宏任务)。
  • 原理:JS是单线程,先执行同步代码,再执行微任务(Promise.then、process.nextTick),最后执行宏任务(setTimeout、setInterval、DOM事件)。

7. 最近一个项目的业务流程及负责事项

  • 业务流程:以电商管理系统为例,用户端包括商品浏览→加入购物车→下单→支付→物流跟踪;管理端包括商品管理→订单处理→库存更新→数据分析。
  • 个人职责
    • 负责用户端购物车模块(添加、删除、数量修改、价格计算)。
    • 实现订单确认页的表单验证和提交逻辑。
    • 对接支付接口,处理支付状态回调。
    • 优化商品列表加载性能(懒加载、缓存)。

8. 权限

  • 路由权限:基于用户角色(如admin、user)配置可访问路由,在router.beforeEach中拦截,无权限则跳转至403页。
  • 按钮权限:通过自定义指令(如v-permission)判断用户是否有操作权限,无则隐藏按钮。
  • 接口权限:请求头携带token,后端验证token有效性,无效则返回401,前端跳转至登录页。

9. 双签怎么做的

双签指同一操作需两人签名确认(如审批流程),实现方式:

  • 前端:设计双签表单,记录第一次签名人、时间,第二次签名需验证第一次签名已完成,提交时携带两人签名信息。
  • 后端:接口校验两次签名的顺序和权限(如第一次为部门经理,第二次为总监),通过后才执行操作(如订单确认、资金流转)。

10. 埋点怎么做的

  • 手动埋点:在用户行为触发处(如点击按钮、页面跳转)调用埋点函数,收集数据(用户ID、行为类型、时间、页面路径),通过fetch发送至埋点服务器。
  • 自动埋点:基于事件委托监听全局点击事件,过滤需要埋点的元素(如带data-track属性的元素),自动提取埋点参数并发送。
  • 工具:使用百度统计、友盟等第三方工具,通过配置代码实现页面访问量、停留时间等基础埋点。

11. 小程序登录怎么做的

  1. 调用wx.login()获取临时登录凭证code
  2. code发送至后端,后端通过微信接口(https://api.weixin.qq.com/sns/jscode2session)换取openid(用户唯一标识)和session_key
  3. 后端生成自定义token(关联openid),返回给小程序。
  4. 小程序存储tokenwx.setStorageSync),后续请求通过header携带token验证身份。
  5. token过期,重新执行wx.login()刷新。

12. 懒加载怎么做的

  • 图片懒加载
    • 初始给图片src设为占位图,data-src存真实地址。
    • 监听页面滚动或使用IntersectionObserver API,当图片进入视口时,将data-src赋值给src
  • 组件懒加载(Vue):
    • 路由懒加载:const Home = () => import('./Home.vue'),配合webpackChunkName拆分代码。
    • 组件懒加载:通过v-if或动态组件(component: () => import('./Component.vue')),在需要时加载。

13. 敏感信息怎么处理

  • 传输加密:敏感信息(如手机号、身份证号)通过AESRSA加密后再发送(前端加密,后端解密)。
  • 存储安全:不将敏感信息存在localStorage/sessionStorage,必要时用sessionStorage(会话结束后清除),避免持久化存储。
  • 显示脱敏:前端展示时脱敏(如手机号显示为138****5678),避免完整信息暴露。
  • 接口限制:非必要接口不返回敏感信息,如列表接口只返回ID,详情接口按需返回(需权限校验)。

14. 多端开发怎么做的

  • 框架选择:使用uni-appTaroReact Native,一套代码编译到H5、小程序、App等多端。
  • 适配处理:通过条件编译(如#ifdef MP-WEIXIN)处理不同端的差异(如小程序的wx.xxx和H5的window.xxx)。
  • 组件复用:封装跨端通用组件(如按钮、表单),针对端差异通过props或样式适配(如rpx适配小程序,rem适配H5)。

15. 怎么判断一个请求是H5端发起的,还是PC端发起的

通过请求头的User-Agent判断:

  • PC端(如Chrome):User-Agent包含Windows NTMacintosh等系统标识,且无移动设备关键词。
  • H5端(移动端浏览器):User-Agent包含MobileAndroidiPhone等移动设备标识。
  • 前端可在请求拦截器中获取navigator.userAgent,添加到请求头;后端解析User-Agent判断端类型。

16. 你的优势是什么

  • 技术扎实:熟悉Vue、React生态,能独立完成从需求到上线的全流程开发。
  • 解决问题能力:面对复杂bug(如性能瓶颈、跨域问题),能通过调试工具和源码分析定位并解决。
  • 沟通协作:与产品、后端高效沟通,明确需求边界,推动项目落地(如曾协调后端优化接口,减少前端30%请求量)。

17. 与后端发生冲突怎么做

  • 先明确冲突点(如接口字段格式、返回逻辑),基于需求文档和用户体验分析问题。
  • 举例说明:若后端认为某个字段无需返回,而前端需要用于展示,会提供具体业务场景(如用户需要查看该信息),协商是否可增加字段或调整返回逻辑。
  • 若无法达成一致,向上级或产品经理反馈,以用户需求和项目目标为优先级决策。

18. 内边距,外边距是什么

  • 内边距(padding):元素边框与内容之间的距离,影响元素内部空间,会增加元素的总宽度/高度(除非设置box-sizing: border-box)。
  • 外边距(margin):元素边框与外部元素之间的距离,用于控制元素与其他元素的间距,可能产生“外边距合并”(如相邻元素的上下margin会取最大值)。

19. 前端怎么申明变量,区别

  • var:函数级作用域,存在变量提升,可重复声明,值可修改。
  • let:块级作用域({}内),无变量提升,不可重复声明,值可修改。
  • const:块级作用域,无变量提升,不可重复声明,声明时必须赋值,值不可修改(引用类型的属性可改)。

20. 前端有哪些异步函数,怎么处理异步

  • 异步函数setTimeoutsetIntervalfetchaxiosPromiseasync/await、DOM事件(如onclick)、Node.js的fs.readFile等。
  • 处理方式
    • 回调函数:如setTimeout(() => {}, 1000)(易导致回调地狱)。
    • Promise:通过then链式调用处理异步,catch捕获错误。
    • async/await:语法糖,用同步写法处理异步(需配合Promise)。

21. 你知道回调地狱吗,有什么危害,怎么解决

  • 回调地狱:多层异步操作嵌套(如setTimeout内嵌套setTimeout),代码可读性差、维护困难、错误处理繁琐。
  • 危害:逻辑混乱,调试困难,不易扩展(如新增一个异步步骤需修改多层嵌套)。
  • 解决
    • Promise链式调用:promise.then().then().catch()
    • async/await:将异步代码写成同步形式(async function() { await promise; })。

22. 怎么防止SQL注入

  • 前端:输入验证(如限制特殊字符';or等),但仅前端验证不足,需后端配合。
  • 核心:后端使用参数化查询(如PreparedStatement),避免直接拼接SQL语句;使用ORM框架(如MyBatis、Hibernate)自动处理参数转义。

23. 怎么防止XSS、DDoS网络攻击

  • XSS(跨站脚本)
    • 输入过滤:过滤<script>等危险标签,转义特殊字符(如<&lt;)。
    • 输出编码:展示用户输入时进行HTML转义(如Vue的v-text默认转义)。
    • 安全策略:设置Content-Security-Policy(CSP)限制脚本来源。
  • DDoS(分布式拒绝服务)
    • 前端:无直接解决方式,可通过CDN分流、限制单IP请求频率(如防抖节流)。
    • 后端/服务器:部署防火墙、负载均衡、流量清洗,使用云服务商的抗DDoS服务。

24. 怎么处理大规模的并发,你之前项目中有遇到吗,讲讲场景,怎么解决的

  • 处理方式
    • 前端:请求合并(如将多个列表请求合并为一个)、缓存(localStorage缓存非实时数据)、防抖节流(限制高频操作如搜索输入)、懒加载(减少初始请求量)。
    • 后端:接口限流、负载均衡、数据库读写分离、缓存(Redis)。
  • 场景:电商秒杀活动,大量用户同时抢购商品,前端频繁发送库存查询请求。
  • 解决:前端对库存查询做节流(1秒内只发1次请求),并显示“加载中”避免重复点击;后端接口限流,超过阈值返回“请求过频”,用Redis缓存实时库存减少数据库压力。

25. 有做过票务系统吗

(根据实际情况回答,若无则说明了解的业务场景)
例:未直接开发,但了解票务系统核心模块:用户购票(选座、支付)、订单管理(出票、退票、改签)、库存管理(实时更新余票)。技术上需解决高并发(如抢票)、数据一致性(避免超售),可通过前端限流、后端锁机制(如Redis分布式锁)实现。

26. 有做过电商系统吗,讲讲你做的

例:做过生鲜电商小程序,负责:

  • 商品模块:分类展示、详情页(规格选择、库存实时展示)、搜索(防抖+联想词)。
  • 购物车:添加/删除商品、勾选状态同步、价格实时计算(使用Vuex管理状态)。
  • 订单:地址选择、支付集成(微信支付)、订单状态追踪(WebSocket实时更新物流)。
  • 优化:商品图片懒加载、列表页缓存、订单页预加载提升体验。

27. is、ts区别

  • is:TypeScript中的类型谓词,用于在函数返回值中明确参数的类型(如function isString(x: any): x is string { return typeof x === 'string' }),帮助TS类型推断。
  • TS(TypeScript):JS的超集,添加静态类型系统,编译时检查类型错误,提升代码可维护性和可读性。

28. 数据安全你做过哪些

  • 传输安全:所有接口使用HTTPS,敏感信息(如密码)通过RSA加密后传输。
  • 存储安全:不存储明文密码,localStorage只存非敏感数据(如用户名),sessionStorage存储临时token并在退出时清除。
  • 权限控制:基于角色的访问控制(RBAC),严格限制数据访问范围(如普通用户不能查看他人订单)。

29. 有做过小程序吗

(根据实际情况回答)
例:做过餐饮外卖小程序,功能包括:首页(店铺列表、推荐菜品)、购物车(加购、结算)、订单(下单、支付、评价)、个人中心(地址管理、订单历史)。技术上使用微信原生框架,封装了请求工具(处理token、错误提示),优化了图片加载速度(压缩+懒加载)。

30. 有没有做过html转译

  • 做过,用于展示用户输入的富文本(如评论中的HTML标签),避免XSS攻击。
  • 实现:使用DOMPurify库过滤危险标签和属性,或手动编写转译函数(如将<转义为&lt;>转义为&gt;)。Vue中可用v-html展示转译后的安全内容。

31. antd有用过吗,与其他框架比感觉怎么样

  • 用过Ant Design(React)和Ant Design Vue,主要用于中后台系统。
  • 优势:组件丰富(表格、表单、弹窗等)、设计规范统一、文档完善、支持按需加载;支持主题定制,适配多端。
  • 对比其他框架(如Element UI):AntD更偏向企业级应用,组件更复杂(如可编辑表格);Element UI更轻量,上手门槛低。

32. 前端崩溃怎么办,如何优化

  • 崩溃处理
    • 监控:使用window.onerrorwindow.addEventListener('unhandledrejection')捕获错误,发送到监控平台(如Sentry)。
    • 恢复:关键页面实现“崩溃重启”(如通过localStorage记录状态,重启后恢复)。
  • 优化
    • 代码层面:减少冗余代码、避免内存泄漏(如及时解绑事件监听)、拆分大组件。
    • 性能层面:优化首屏加载(懒加载、代码分割)、减少重绘重排(批量操作DOM)、使用Web Worker处理复杂计算。

33. diff算法

  • 定义:虚拟DOM(VNode)的对比算法,用于找出新旧VNode的差异,只更新差异部分(而非全量渲染),提升性能。
  • Vue的diff优化:
    • 只对比同级节点(避免跨层级对比,降低复杂度)。
    • key标识节点(key相同则复用节点,减少DOM操作)。
    • 双指针遍历:新旧节点列表两端向中间对比,快速定位可复用节点。

34. 闭包

  • 定义:函数嵌套函数,内部函数引用外部函数的变量,外部函数执行后,变量仍被内部函数保留(不被垃圾回收)。
  • 用途:封装私有变量(如计数器)、模块化(避免全局污染)、柯里化(函数参数复用)。
  • 注意:过度使用会导致内存泄漏(变量长期不释放),需及时清除引用(如赋值null)。

35. es6(重点特性)

  • 变量声明:let/const(块级作用域)。
  • 函数:箭头函数(() => {},无this绑定)、默认参数、剩余参数(...args)。
  • 数组/对象:解构赋值(const {a} = obj)、扩展运算符(...)、Object.assign
  • 其他:Promise(异步处理)、class(类语法)、模块化(import/export)、Set/Map(数据结构)。

36. 双向数据绑定原理

  • Vue 2:基于Object.defineProperty劫持对象属性的gettersetter
    • 初始化时,对data中的属性递归劫持,触发getter时收集依赖(Watcher)。
    • 属性更新时,触发setter,通知依赖的Watcher更新视图。
  • Vue 3:基于Proxy代理整个对象,支持监听数组索引变化、新增属性,解决了Vue 2的局限性(如无法监听数组长度修改)。

37. 插槽,项目中怎么用的

  • 插槽:Vue中用于组件内容分发的机制,让父组件可以向子组件插入自定义内容。
  • 分类
    • 匿名插槽:子组件用<slot>接收父组件的默认内容。
    • 具名插槽:子组件用<slot name="header">,父组件用<template #header>指定插入位置(如页面布局的header、content、footer)。
    • 作用域插槽:子组件通过<slot :data="list">传递数据,父组件用<template #default="scope">接收并自定义渲染(如表格列的自定义展示)。

38. vuex,项目中怎么用的

  • Vuex:Vue的状态管理模式,集中管理组件共享状态(如用户信息、购物车数据)。
  • 核心模块state(状态)、mutations(同步修改状态)、actions(异步操作,提交mutations)、getters(计算派生状态)、modules(拆分模块)。
  • 项目使用:电商项目中,购物车数据存在Vuex的state,添加商品调用actions(异步请求后端),再通过mutations更新state;组件通过mapState/mapGetters获取数据,mapActions调用方法。

39. 懒加载有哪几种方式,具体实现

  • 图片懒加载
    • 实现:给图片设置data-src(真实地址),初始src为占位图;监听页面滚动或用IntersectionObserver API,当图片进入视口时,将data-src赋值给src
  • 路由懒加载(Vue):
    • 实现:const Home = () => import(/* webpackChunkName: "home" */ './Home.vue'),Webpack会将组件打包为单独的chunk,访问路由时才加载。
  • 组件懒加载
    • 实现:通过v-if控制组件加载时机(如点击按钮后加载),或用defineAsyncComponent(Vue 3)动态导入组件。

40. h5,移动端pc端如何适配

  • H5/移动端
    • 方案1:rem适配,设置htmlfont-size为屏幕宽度的1/10(如750px设计稿,font-size: 75px),元素尺寸用rem(1rem=75px)。
    • 方案2:vw/vh,1vw=屏幕宽度的1%,直接用vw设置尺寸(如设计稿宽度750px,元素宽度100px→100/750*100=13.33vw)。
    • 工具:PostCSS的postcss-px-to-viewport自动将px转为vw。
  • PC端:响应式布局,用媒体查询(@media)适配不同屏幕宽度(如@media (max-width: 1200px) { ... }),结合弹性布局(flex)和网格布局(grid)。

41. 封装过哪些组件,考虑的点是什么

  • 封装组件:通用按钮(MyButton)、表单输入框(MyInput)、弹窗(MyModal)、表格(MyTable)。
  • 考虑点
    • 可复用性:通过props接收参数(如按钮的typesize),emit事件传递交互(如点击事件)。
    • 可扩展性:支持插槽(如弹窗的头部/底部自定义内容)、自定义样式(class/style传入)。
    • 健壮性:处理边界情况(如输入框为空时的提示)、添加默认值。
    • 易用性:文档说明用法,提供示例;兼容不同场景(如表格支持排序、筛选)。

42. 自定义指令

  • 定义:Vue中扩展HTML元素功能的方式,分为全局指令和局部指令。
  • 示例:自定义v-focus指令,实现输入框自动聚焦:
    // 全局注册
    Vue.directive('focus', {
      inserted(el) { el.focus(); } // 元素插入DOM时执行
    });
    // 使用:<input v-focus>
    
  • 项目使用:权限控制指令v-permission(无权限时隐藏元素)、长按指令v-longpress(触发长按事件)。

43. 父子组件生命周期执行顺序

  • 加载阶段:父beforeCreate→父created→父beforeMount→子beforeCreate→子created→子beforeMount→子mounted→父mounted
  • 更新阶段:父beforeUpdate→子beforeUpdate→子updated→父updated
  • 销毁阶段:父beforeDestroy→子beforeDestroy→子destroyed→父destroyed

44. nextTick怎么用,实现原理,返回一个什么,源码看过吗

  • 用法:在DOM更新完成后执行回调,如修改数据后获取更新后的DOM尺寸:
    this.msg = 'new message';
    this.$nextTick(() => { console.log(this.$el.textContent); }); // 输出'new message'
    
  • 实现原理:利用微任务(优先Promise.then,兼容MutationObserversetImmediate)等待DOM更新队列执行完毕后,再执行回调。
  • 返回值:返回一个Promise对象(Vue 2.1+),可配合async/await使用。
  • 源码:大致逻辑是将回调函数放入回调队列,通过timerFunc(微任务/宏任务)触发队列执行,确保在DOM更新后调用。

45. promise(深)

  • 概念:用于处理异步操作的对象,有3种状态:pending(进行中)、fulfilled(成功)、rejected(失败),状态一旦改变不可逆。
  • 方法
    • Promise.resolve():返回成功状态的Promise。
    • Promise.reject():返回失败状态的Promise。
    • Promise.all([p1, p2]):全部成功则返回结果数组,有一个失败则立即失败。
    • Promise.race([p1, p2]):第一个改变状态的Promise决定结果。
    • Promise.allSettled([p1, p2]):所有Promise完成后返回结果(无论成功失败)。
  • 实现:手动实现简易Promise,需包含then方法、状态管理、回调队列。

46. async await与promise区别

  • 关系async/awaitPromise的语法糖,基于Promise实现。
  • 区别
    • 写法:async/await用同步代码风格写异步(如const res = await promise);Promisethen链式调用。
    • 错误处理:async/awaittry/catch捕获错误;Promisecatch方法。
    • 调试:async/await调试时可逐行执行;Promisethen回调在单独的函数中,调试稍复杂。
    • 适用场景:async/await更适合处理串行异步(如依赖前一个请求结果的多个请求);Promise.all更适合并行异步。

47. axios怎么封装,如何取消请求

  • 封装
    import axios from 'axios';
    const instance = axios.create({ baseURL: '/api', timeout: 5000 });
    // 请求拦截器:添加token
    instance.interceptors.request.use(config => {
      config.headers.token = localStorage.getItem('token');
      return config;
    });
    // 响应拦截器:处理错误
    instance.interceptors.response.use(
      res => res.data,
      err => { if (err.response.status === 401) { /* 跳转登录 */ } }
    );
    export default instance;
    
  • 取消请求
    • 方式1:用CancelToken(axios v0.x):
      const source = axios.CancelToken.source();
      instance.get('/data', { cancelToken: source.token });
      // 取消请求
      source.cancel('取消原因');
      
    • 方式2:用AbortController(axios v0.22+,推荐):
      const controller = new AbortController();
      instance.get('/data', { signal: controller.signal });
      // 取消请求
      controller.abort();
      

48. 同源策略

  • 定义:浏览器的安全策略,限制不同源(协议、域名、端口任一不同)的文档或脚本交互。
  • 限制:不能读取对方的Cookie、LocalStorage,不能发送AJAX请求,不能操作DOM。
  • 目的:防止恶意网站窃取数据(如通过iframe嵌入银行页面获取用户信息)。

49. cookie怎么在跨域之间共享

  • 默认情况下,Cookie不允许跨域共享,需后端配合设置响应头:
    • Access-Control-Allow-Credentials: true(允许跨域请求携带Cookie)。
    • Set-Cookie中添加SameSite=None; SecureSameSite=None允许跨站,Secure要求HTTPS)。
  • 前端请求需设置withCredentials: true(axios中{ withCredentials: true })。

50. 解决跨域的方式

  • CORS(跨域资源共享):后端设置Access-Control-Allow-Origin: 前端域名,支持所有HTTP方法,是最常用方式。
  • 代理服务器:开发环境用Webpack Dev Server代理(proxy: { '/api': { target: '后端域名', changeOrigin: true } }),生产环境用Nginx代理。
  • JSONP:利用<script>标签无跨域限制,仅支持GET请求,后端返回callback(data)格式的脚本。
  • iframe + postMessage:跨域页面通过postMessage传递数据(如父页面iframe.contentWindow.postMessage(data, '目标域名'),子页面监听message事件)。

51. 项目优化

  • 加载优化
    • 代码分割:路由懒加载、组件懒加载(减少初始JS体积)。
    • 资源压缩:Webpack压缩JS/CSS,图片压缩(WebP格式)、使用CDN。
    • 缓存:HTTP缓存(Cache-Control)、localStorage缓存静态数据。
  • 运行时优化
    • DOM优化:批量操作DOM(documentFragment)、减少重绘重排(用transform/opacity触发合成层)。
    • 事件优化:事件委托(减少事件监听数量)、防抖节流(如滚动、搜索)。
    • Vue优化:v-forkeyv-ifv-for不共存、keep-alive缓存组件、避免不必要的响应式数据(Object.freeze)。

52. 检测数据类型的方式

  • typeof:判断基本类型(string/number/boolean/undefined/symbol/function),但null返回object,数组返回object
  • instanceof:判断引用类型(如arr instanceof Array),基于原型链判断,不能判断基本类型。
  • Object.prototype.toString.call():通用方式,返回[object 类型](如toString.call(null) → "[object Null]"toString.call([]) → "[object Array]")。
  • Array.isArray():专门判断数组(比instanceof更可靠,因instanceof受原型链影响)。

53. 数组遍历的方法,map foreach区别

  • 遍历方法forEachmapfilterfindsomeeveryreduce等。
  • map vs forEach
    • 返回值:map返回新数组(每个元素为回调返回值);forEach无返回值(undefined)。
    • 中断遍历:均不能用break中断(forEach可用try/catch配合throw中断,但不推荐)。
    • 用途:map用于转换数组(如将数组元素乘以2);forEach用于单纯遍历(如打印元素)。

54. 前端怎么批量处理请求并发

  • Promise.all:处理多个并行请求,全部成功后返回结果数组(如同时获取用户信息、商品列表、分类数据)。
    const p1 = axios.get('/user');
    const p2 = axios.get('/goods');
    const [user, goods] = await Promise.all([p1, p2]);
    
  • 控制并发数量:用Promise池限制同时发起的请求数(如一次最多发5个请求,避免请求过多导致浏览器/后端压力)。

55. 讲讲你印象最深刻的项目,你负责什么

例:生鲜电商小程序(日活10万+),负责核心下单流程:

  • 挑战:高峰期(早8点)下单请求集中,库存更新延迟导致超售。
  • 解决:前端做库存预校验(下单时先查缓存库存),后端用Redis分布式锁保证库存原子性更新;前端添加加载状态和排队提示,优化用户体验。
  • 成果:超售率从5%降至0.1%,下单转化率提升15%。

56. 面向对象,面向过程

  • 面向过程:以步骤为中心,按顺序执行函数(如实现登录:getUserInput()validate()submit()),适合简单逻辑。
  • 面向对象(OOP):以对象为中心,将数据和方法封装到对象(如User类有name属性和login()方法),通过继承、多态复用代码,适合复杂系统。

57. Vue是面向对象还是过程,为什么

  • Vue是基于面向对象思想设计的框架:
    • 组件本质是对象(包含data/methods/props等属性和方法)。
    • 内部实现大量使用类(如Vue类、Watcher类、VNode类),通过实例化对象管理状态和DOM。
    • 支持混入(mixin)、继承(组件扩展)等OOP特性。

58. Vue页面开发是面向对象还是过程,为什么

  • 面向对象
    • 页面(组件)是对象实例,data是状态,methods是方法,computed是计算属性,符合OOP的封装特性。
    • 组件间通过props/emit/Vuex通信,类似对象间的消息传递。
    • 但开发过程中按业务流程编写代码(如先渲染列表,再处理点击事件),也包含过程式思想,是两者的结合。

59. 前端发请求到后端整个过程,越详细越好

  1. 前端通过axios/fetch构造请求(method/url/headers/data)。
  2. 浏览器检查是否跨域:
    • 同域:直接发送请求。
    • 跨域:先发送OPTIONS预检请求(检查后端是否允许跨域),通过后再发真实请求。
  3. 请求经网络传输到后端服务器(如Nginx),服务器转发到应用服务器(如Node.js/Java)。
  4. 后端验证请求(token、权限),处理业务逻辑(如查询数据库)。
  5. 后端返回响应(status/headers/body),经网络传回前端。
  6. 浏览器接收响应,前端响应拦截器处理数据(如解析JSON、错误处理)。
  7. 前端更新页面(如Vue更新data触发视图渲染)。

60. 前端是如何精确找到后端的,原理是什么

  • 原理:通过URL(统一资源定位符)定位后端接口,URL包含:
    • 协议(http/https):规定数据传输方式。
    • 域名/IP:定位服务器(如api.example.com通过DNS解析为服务器IP)。
    • 端口:默认80(http)/443(https),可自定义(如:3000)。
    • 路径:定位具体接口(如/user/login)。
  • 过程:前端请求的url(如https://api.example.com:8080/user/login)经DNS解析为服务器IP,通过IP和端口建立TCP连接,发送请求数据。

61. 有没有做过项目部署

  • 做过,流程:
    1. 本地打包:npm run build生成dist目录(静态资源)。
    2. 服务器准备:购买云服务器(如阿里云ECS),安装Nginx。
    3. 上传文件:通过scp/FTP将dist上传到服务器(如/usr/share/nginx/html)。
    4. 配置Nginx:设置根目录(root /usr/share/nginx/html)、端口(listen 80)、跨域代理(proxy_pass)。
    5. 启动Nginx:systemctl start nginx,访问服务器IP即可看到页面。

62. 你的代理是怎么做的

  • 开发环境(Vue CLI):在vue.config.js配置代理:
    module.exports = {
      devServer: {
        proxy: {
          '/api': {
            target: 'http://backend.example.com', // 后端地址
            changeOrigin: true, // 欺骗后端为同域
            pathRewrite: { '^/api': '' } // 去掉请求中的/api前缀
          }
        }
      }
    };
    
  • 生产环境:Nginx配置代理:
    location /api/ {
      proxy_pass http://backend.example.com/;
      proxy_set_header Host $host;
    }
    

63. Chrome浏览器有哪些东西,都有什么作用

  • 地址栏:输入URL/搜索,显示当前页面信息(如HTTPS锁标识)。
  • 标签页:每个标签页对应一个页面,支持拖拽、分组。
  • 开发者工具(F12)
    • Elements:查看/编辑DOM和CSS。
    • Console:执行JS、查看日志。
    • Network:监控请求(时间、响应)。
    • Performance:分析页面性能(加载/运行时间)。
    • Application:管理存储(Cookie/LocalStorage)、缓存、Service Worker。
  • 扩展程序:如Vue Devtools(调试Vue)、AdBlock(屏蔽广告)。
  • 设置:管理隐私、安全、外观等(如清除缓存、设置默认搜索引擎)。

64. 一个页面有50个组件,某个组件卡了,怎么精确定位,怎么解决

  • 定位
    1. 用Chrome Performance面板录制页面操作,查看各组件的渲染/脚本执行时间,定位耗时最长的组件。
    2. 在组件的mounted/updated中用console.time/console.timeEnd打印执行时间。
    3. 检查组件是否有大量DOM操作(如频繁更新列表)、复杂计算(如大数据过滤)、内存泄漏(如未解绑的事件监听)。
  • 解决
    • 优化渲染:减少DOM节点(如虚拟列表)、避免频繁重绘(用v-show代替v-if)。
    • 优化计算:将复杂计算移到created(只执行一次)、用Web Worker处理(不阻塞主线程)。
    • 修复内存泄漏:及时解绑事件(beforeDestroyoff事件)、清除定时器。

65. 响应式是什么,原理是什么,有哪些副作用

  • 响应式:数据变化时,视图自动更新(如Vue的data修改后页面同步变化)。
  • 原理(Vue 2):Object.defineProperty劫持data属性的getter(收集依赖)和setter(通知更新);Vue 3用Proxy代理对象。
  • 副作用
    • 性能消耗:劫持所有属性会增加初始化时间,尤其数据量大时。
    • 局限性:Vue 2无法监听数组索引/长度修改、对象新增属性(需Vue.set);Proxy兼容性稍差(不支持IE)。
    • 调试困难:数据自动更新可能导致难以追踪的bug(如意外修改数据)。

66. 有没有接触过因为响应式出现的bug,业务场景,怎么解决的

  • 场景:Vue 2中,给对象新增属性(this.obj.newProp = 1),视图不更新。
  • 原因:Vue 2的Object.defineProperty无法劫持新增属性,未收集该属性的依赖。
  • 解决:用this.$set(this.obj, 'newProp', 1)强制触发响应式;或初始化时定义所有可能的属性(obj: { newProp: null })。

67. 项目的亮点,难点,怎么解决的

  • 亮点:电商项目中实现了“购物车离线编辑”,用户断网时可添加/删除商品,联网后自动同步到后端(用localStorage缓存离线操作,navigator.onLine监听网络状态)。
  • 难点:多端(H5/小程序)共用一套购物车逻辑,不同端的存储API不同(H5用localStorage,小程序用wx.setStorage)。
  • 解决:封装适配层(storage.js),统一get/set/remove方法,内部判断端类型调用对应API,上层逻辑无需关心端差异。

68. 介绍一下你做的项目

(以电商小程序为例)

  • 项目名称:生鲜到家小程序(用户端+管理端)。
  • 技术栈:Vue 2 + 微信小程序原生框架 + Node.js(部分接口) + MySQL。
  • 核心功能:
    • 用户端:商品浏览、分类筛选、购物车、下单支付、订单追踪。
    • 管理端:商品上下架、订单处理、库存管理、销售报表。
  • 个人职责:负责购物车和订单模块,优化了下单流程(减少3步操作),提升支付转化率10%;解决了库存超售问题(前端限流+后端锁机制)。

69. js数据类型

  • 基本类型stringnumberbooleanundefinednullsymbol(ES6)、bigint(ES10)。
  • 引用类型object(包括arrayfunctiondateregexp等)。
  • 区别:基本类型存储在栈内存,值不可变;引用类型存储在堆内存,栈中存地址,值可修改。

70. 原型,原型链

  • 原型(prototype):函数的属性,是一个对象,用于存放实例共享的方法和属性(如Array.prototypepush方法)。
  • 原型链:对象查找属性时,若自身没有,会沿着__proto__(隐式原型)向上查找父对象的原型,直到null,形成的链条即原型链。
  • 例:arr.__proto__ === Array.prototypeArray.prototype.__proto__ === Object.prototypeObject.prototype.__proto__ === null

71. 继承,实现继承的方式

  • 原型链继承:子类原型指向父类实例(Child.prototype = new Parent()),缺点:父类实例属性被所有子类实例共享。
  • 构造函数继承:子类构造函数中调用父类构造函数(Parent.call(this)),解决属性共享问题,但无法继承原型方法。
  • 组合继承:结合原型链和构造函数(Parent.call(this) + Child.prototype = new Parent()),但父类构造函数执行两次。
  • ES6 class继承class Child extends Parent { constructor() { super(); } },语法简洁,本质是原型继承的语法糖。

72. class

  • ES6的类语法,用于创建对象模板,替代传统的构造函数+原型方式。
  • 基本用法:
    class Person {
      constructor(name) { this.name = name; } // 构造函数
      sayHi() { console.log(`Hi, ${this.name}`); } // 实例方法
      static create(name) { return new Person(name); } // 静态方法
    }
    const p = new Person('Tom');
    p.sayHi(); // "Hi, Tom"
    
  • 特性:支持继承(extends)、super关键字(调用父类方法)、私有属性(#name,ES2022)。

73. 垃圾回收机制

  • JS自动管理内存的机制,回收不再使用的内存。
  • 回收算法
    • 标记清除:标记不再引用的对象(如超出作用域的变量),定期清除。
    • 引用计数:记录对象被引用的次数,次数为0时回收,缺点:无法解决循环引用(A引用B,B引用A,次数永远不为0)。
  • 避免内存泄漏:及时清除定时器、解绑事件监听、置空不再使用的对象(obj = null)。

74. eventloop(事件循环)

  • JS处理异步操作的机制(单线程+任务队列)。
  • 流程
    1. 执行同步代码,将异步任务(宏任务/微任务)放入对应的队列。
    2. 同步代码执行完,执行所有微任务(Promise.thenprocess.nextTick)。
    3. 微任务执行完,渲染页面,执行一个宏任务(setTimeoutDOM事件)。
    4. 重复步骤2-3,形成循环。
  • 例:setTimeout(() => console.log(1), 0); Promise.resolve().then(() => console.log(2)); console.log(3) → 输出3→2→1。

75. mvvm

  • MVVM:Model-View-ViewModel的缩写,是一种架构模式。
  • 组成
    • Model:数据模型(如后端返回的数据、data中的数据)。
    • View:视图(DOM,用户看到的界面)。
    • ViewModel:连接Model和View的桥梁,监听Model变化更新View,监听View事件更新Model(如Vue的实例)。
  • 优势:分离视图和数据,减少DOM操作,提高开发效率(如Vue中修改data自动更新视图)。

76. 存储方式

  • Cookie:容量4KB,随请求发送,有过期时间,可设置httpOnly(防JS读取)、secure(仅HTTPS传输)。
  • localStorage:容量5-10MB,持久化存储(关闭浏览器不丢失),需手动清除,同域共享。
  • sessionStorage:容量5-10MB,会话级存储(关闭标签页丢失),同标签页共享。
  • indexedDB:大容量(理论无上限),异步操作,适合存储大量结构化数据。

77. webpack打包,有没有做过改动或者优化

  • 改动
    • 配置entry/output指定入口出口,module.rules配置 loader(如babel-loader转ES6,css-loader处理CSS)。
    • 插件:HtmlWebpackPlugin生成HTML,MiniCssExtractPlugin提取CSS为单独文件,CleanWebpackPlugin清理dist目录。
  • 优化
    • 代码分割:splitChunks提取公共代码(如node_modules)。
    • 压缩:TerserPlugin压缩JS,CssMinimizerPlugin压缩CSS。
    • 懒加载:通过import()动态导入,配合webpackChunkName命名chunk。
    • 缓存:给输出文件名加hash([contenthash]),实现浏览器缓存。

78. cdn为什么没有跨域

  • CDN(内容分发网络)的资源(如JS、图片)通常通过<script><img>等标签加载,这些标签不受同源策略限制(同源策略主要限制AJAX请求)。
  • 若CDN资源提供AJAX接口,仍需后端设置CORS才能跨域访问。

79. 如何知道一个dom在不在可视区域内

  • 方法1getBoundingClientRect():获取元素位置,判断top < window.innerHeight && bottom > 0 && left < window.innerWidth && right > 0
  • 方法2IntersectionObserver API(推荐):监听元素与视口的交叉状态,异步触发回调,性能更好。
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => { if (entry.isIntersecting) { console.log('在可视区域'); } });
    });
    observer.observe(dom); // 监听元素
    

80. 大文件上传如何实现

  • 分片上传
    1. 前端将文件按固定大小(如5MB)拆分为分片(File.slice(start, end))。
    2. 每个分片带chunkIndex(序号)、fileNametotalChunks(总分片数)发送到后端。
    3. 后端接收分片并暂存,所有分片上传完成后,合并为完整文件。
  • 断点续传
    • 上传前请求后端已上传的分片索引,前端只传未上传的分片(基于localStorage记录已传分片)。
  • 进度显示:监听XMLHttpRequest.upload.onprogress获取上传进度。

81. pdf渲染怎么做的

  • pdf.js(Mozilla开源库):
    1. 引入pdf.jspdf.worker.js
    2. 加载PDF文件:pdfjsLib.getDocument(url).promise.then(pdf => { ... })
    3. 渲染页面:pdf.getPage(pageNum).then(page => { page.render({ canvasContext: ctx, viewport: page.getViewport({ scale: 1 }) }); })
  • 优势:纯前端渲染,支持分页、缩放、搜索,兼容性好。

82. 你会什么,你擅长什么,你有哪些不足

  • 会的技能:Vue/React生态、HTML/CSS/JS、小程序开发、Webpack、Axios封装、项目优化。
  • 擅长:复杂业务逻辑实现(如电商下单流程)、前端性能优化(曾将页面加载时间从3s优化到1s)、跨端适配(H5/小程序统一开发)。
  • 不足:后端知识较浅(如数据库优化),正在学习Node.js和MySQL;对底层源码(如Vue响应式)理解不够深入,计划深入阅读源码。

83. 如何与客户去沟通,当需求频繁变更,你如何处理,当你感觉客户需求不对的时候该怎么办

  • 与客户沟通:用通俗语言解释技术实现,定期同步进度(如周报),确认需求细节(避免歧义),记录沟通结果(邮件/文档)。
  • 需求频繁变更
    • 评估变更影响(开发时间、成本),反馈给客户和产品经理。
    • 建议迭代开发:核心功能优先上线,变更需求放入下一版本,避免打乱当前开发计划。
  • 需求不对时
    • 提供数据或案例说明问题(如“该需求可能导致用户操作复杂,同类产品通常这样设计…”)。
    • 提出替代方案,与客户、产品经理讨论,以用户体验和项目目标为核心决策。

84. 当你工作出错你该怎么办,有没有带过团队,能不能带团队,你之前项目多少人,你负责什么

  • 出错处理:立即止损(如回滚代码),分析原因(技术漏洞/需求理解错),修复后测试验证,记录到知识库避免重复犯错,向团队和上级坦诚汇报。
  • 团队经验:曾带领3人小组开发电商小程序,负责任务分配(按成员擅长领域分工)、技术难点攻克、代码审查,按时完成上线,团队效率提升20%。
  • 带团队能力:能协调资源、解决冲突、推动进度,注重代码规范和知识共享(如组织周会分享技术点)。
  • 之前项目规模:6人团队(2前端、3后端、1产品),我负责前端核心模块开发和技术选型。

85. ts中typeof和instance区别

  • typeof:用于获取变量的类型(编译时),返回类型字面量(如typeof 'a' → 'string'),可用于类型推断(type T = typeof obj)。
  • instanceof:JS运行时运算符,判断对象是否为某个构造函数的实例(如obj instanceof Array),TS中用于类型守卫(缩小类型范围)。
  • 例:typeof是类型层面的操作,instanceof是值层面的操作。

86. v3组件传值

  • 父传子propsdefineProps定义),子组件通过props接收。
    <!-- 父组件 -->
    <Child :msg="message" />
    <!-- 子组件 -->
    <script setup> const props = defineProps({ msg: String }); </script>
    
  • 子传父emitdefineEmits定义),子组件触发事件,父组件监听。
    <!-- 子组件 -->
    <script setup> const emit = defineEmits(['change']); emit('change', value); </script>
    <!-- 父组件 -->
    <Child @change="handleChange" />
    
  • 跨组件provide/inject(祖先组件提供数据,后代组件注入)、Vuex/Pinia(状态管理)。

87. 改变this指向的方式

  • call:参数列表(func.call(obj, arg1, arg2)),立即执行函数。
  • apply:参数数组(func.apply(obj, [arg1, arg2])),立即执行函数。
  • bind:返回新函数(const newFunc = func.bind(obj, arg1)),需手动调用,可柯里化。
  • 箭头函数:无this绑定,继承外层作用域的this,无法通过上述方法改变。

88. data为啥是个函数

  • Vue组件中data必须是函数,返回一个对象:
    • 原因:组件可被多次实例化,函数每次返回新对象,避免多个实例共享同一data(若为对象,修改一个实例的data会影响其他实例)。
    • 例:data() { return { count: 0 }; },每个实例的count独立。

89. vmodel原理,如何手写一个vmodel

  • 原理:语法糖,本质是propsvalue) + emitinput事件)的结合。
    <!-- 父组件 -->
    <Child v-model="msg" />
    <!-- 等价于 -->
    <Child :value="msg" @input="msg = $event" />
    
  • 手写vmodel(自定义组件):
    <!-- 子组件 Child.vue -->
    <template> <input :value="value" @input="$emit('input', $event.target.value)" /> </template>
    <script> export default { props: ['value'] }; </script>
    

90. 口述深拷贝,深浅拷贝区别

  • 深浅拷贝:针对引用类型,浅拷贝只复制地址(修改新对象会影响原对象);深拷贝复制值(新对象与原对象完全独立)。
  • 浅拷贝方法Object.assign()、扩展运算符({...obj})、数组slice()/concat()
  • 深拷贝实现
    • 简单版:JSON.parse(JSON.stringify(obj))(缺点:不支持函数、RegExpDate)。
    • 递归版:遍历对象,基本类型直接复制,引用类型递归拷贝(处理数组、对象、循环引用)。

91. set干嘛的

  • Set是ES6的集合数据结构,特点:
    • 成员唯一(无重复值),可用于数组去重([...new Set(arr)])。
    • 无序,不能通过索引访问,通过add()添加,delete()删除,has()判断是否存在,size属性获取长度。
    • 可遍历(for...offorEach),适合存储不重复的无序数据(如用户ID列表)。

92. 代码题:手写 todo list 支持将状态在已完成和待完成之间切换

<div id="app">
  <input type="text" v-model="task" @keyup.enter="addTask">
  <button @click="addTask">添加</button>
  <ul>
    <li v-for="(item, index) in tasks" :key="index" 
        :style="{ textDecoration: item.done ? 'line-through' : 'none' }"
        @click="toggleDone(index)">
      {{ item.name }}
    </li>
  </ul>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
new Vue({
  el: '#app',
  data: { task: '', tasks: [] },
  methods: {
    addTask() {
      if (!this.task.trim()) return;
      this.tasks.push({ name: this.task, done: false });
      this.task = '';
    },
    toggleDone(index) {
      this.tasks[index].done = !this.tasks[index].done;
    }
  }
});
</script>

93. 平时技术学习途径有哪些?

  • 官方文档(Vue/React文档、MDN)、技术博客(掘金、知乎、Medium)、视频课程(极客时间、慕课网)。
  • 开源项目(阅读Vue/axios源码)、技术社区(GitHub、Stack Overflow)、线下技术沙龙。
  • 实践:复现经典功能(如Promise、虚拟DOM)、参与开源贡献(修复小bug)。

94. 开源社区做过什么贡献没? Vue、组件库有参与过没?issue 呢?

(根据实际情况回答,若无则说明计划)
例:暂未直接贡献代码,但在GitHub上给Vue的issue提过建议(如某个文档描述不清晰);给Element UI提交过bug反馈(一个表格排序异常的场景),并附上复现步骤,被官方采纳修复。计划学习源码后,尝试提交PR解决简单问题。

95. ts中any vs never

  • any:任意类型,关闭类型检查,变量可赋值为任何类型,调用任何方法(如const a: any = '1'; a.push(2)不报错)。
  • never:表示永远不会发生的值的类型(如抛出错误的函数、无限循环的函数返回值),不能赋值给任何类型(除never本身)。

96. ts中type vs interface

  • 相同点:都可定义类型(对象、函数),支持扩展(extends)。
  • 区别
    • type:可定义基本类型别名、联合类型、交叉类型(type A = string | number);不可重复声明。
    • interface:只能定义对象/函数类型;可重复声明(自动合并),更适合定义接口(如组件Props)。

97. ts泛型

  • 泛型:定义时不指定具体类型,使用时指定,实现类型复用(如“编写一次,适配多种类型”)。
  • 例:创建一个返回任意类型参数的函数:
    function identity<T>(arg: T): T { return arg; }
    const str = identity<string>('hello'); // T为string
    const num = identity(123); // 自动推断T为number
    
  • 用途:封装通用组件(如Array<T>)、工具函数,保证类型安全。

98. webpack的配置和vite的区别

特性WebpackVite
原理基于打包(将代码编译为bundle)基于ES模块(开发时按需编译)
开发启动速度慢(需打包所有代码)快(只编译请求的模块)
热更新需重新打包部分模块基于原生ESM热更新,更快
配置复杂度高(需配置loader、plugin)低(默认支持Vue/React,零配置)
生产构建成熟(代码分割、压缩优化好)用Rollup打包,优化接近Webpack

99. 组件通信

  • 父子组件props + emit(Vue)、props + callback(React)。
  • 祖孙/跨级组件provide/inject(Vue)、Context(React)、Vuex/Pinia/Redux。
  • 兄弟组件:共享父组件状态(通过父组件中转)、事件总线(EventEmitter)。

100. 自定义指令和场景

  • 场景
    • v-loading:请求加载时显示加载动画(绑定loading状态,inserted时添加动画元素,update时控制显示/隐藏)。
    • v-copy:点击元素复制文本(监听click事件,用document.execCommand('copy')复制value)。
    • v-permission:根据权限控制元素显示(bind时判断权限,无则设置display: none)。

101. 输入url到页面呈现发生了什么

  1. DNS解析:将域名(如www.baidu.com)解析为IP地址(如14.215.177.38)。
  2. TCP连接:通过三次握手建立客户端与服务器的TCP连接。
  3. HTTPS握手(若用HTTPS):交换证书,生成对称加密密钥。
  4. 发送HTTP请求:包含请求行(GET /index.html HTTP/1.1)、请求头、请求体。
  5. 服务器处理请求:返回HTML/CSS/JS等资源。
  6. 浏览器解析渲染
    • HTML解析为DOM树,CSS解析为CSSOM树,合并为渲染树。
    • 布局(计算元素位置大小)、绘制(像素填充)、合成(分层显示)。
  7. 页面交互:加载JS执行(如绑定事件),页面可交互。

102. axios接口封装,如何处理轮询结果先请求后返回问题

  • 接口封装:见第47题。
  • 轮询问题:多次轮询中,先发送的请求后返回,覆盖后发送请求的结果(如实时数据展示错误)。
  • 解决:记录最新请求的标识(如时间戳),只处理最新请求的响应:
    let lastRequestTime = 0;
    async function pollData() {
      const currentTime = Date.now();
      lastRequestTime = currentTime;
      const res = await axios.get('/data');
      if (currentTime === lastRequestTime) { // 只处理最新请求
        updateUI(res.data);
      }
    }
    setInterval(pollData, 1000);
    

103. qiankun不同应用间的操作痕迹缓存

  • qiankun(微前端框架)中,不同子应用的操作痕迹(如表单输入、滚动位置)缓存可通过:
    • 子应用beforeUnmount时,将状态存入全局变量(如window.__GLOBAL_STATE__)。
    • 子应用mount时,从全局变量读取并恢复状态(如document.querySelector('input').value = state.inputVal)。
  • 配合localStorage持久化缓存(如用户未提交的表单,刷新页面后恢复)。

104. 虚拟列表怎么实现的? 开源社区有方案吗?为什么不直接用

  • 实现原理

    1. 计算可视区域高度、单个列表项高度,得出可视区域可显示的最大项数(visibleCount)。
    2. 监听滚动事件,获取滚动距离(scrollTop),计算当前可视区域的起始索引(startIndex = Math.floor(scrollTop / itemHeight))和结束索引(endIndex = startIndex + visibleCount)。
    3. 从数据源中截取startIndexendIndex的列表项,渲染到页面。
    4. 通过transform: translateY(startIndex * itemHeight)设置偏移量,让截取的项显示在可视区域(模拟滚动效果)。
    5. 可选:添加缓冲区(上下各多渲染几项),避免快速滚动时出现空白。
  • 开源方案:有,如react-windowreact-virtualized(React)、vue-virtual-scroller(Vue)、vue3-virtual-list(Vue 3)等,这些库处理了动态高度、横向滚动、性能优化等边缘场景。

  • 不直接用的原因

    • 项目需求简单(如固定高度、数据量不大),自定义实现更轻量(避免引入冗余代码)。
    • 现有库的API与项目架构不兼容,定制化成本高于自研。
    • 学习目的:深入理解原理,用于特定场景(如结合拖拽、复杂嵌套)。

105. 门户所有的页面是如何配置出来的?能力边界在哪里?

  • 页面配置方式

    1. 可视化配置平台:通过拖拽组件(如“轮播图”“新闻列表”)生成页面结构,配置组件属性(如标题、接口地址),保存为JSON配置。
    2. 后端驱动:后端返回页面配置数据(组件类型、布局、数据来源),前端解析JSON动态渲染组件(component: () => import(componentName))。
    3. 混合模式:基础页面用配置生成,复杂页面(如表单、图表)通过代码开发,配置平台关联代码组件。
  • 能力边界

    • 适合:标准化、高频复用的页面(如资讯列表、公告页),需快速迭代、非技术人员可操作的场景。
    • 局限:复杂交互(如多步骤表单校验、实时协作)、高性能要求(如大数据可视化)难以通过配置实现,仍需代码开发;配置灵活性越高,平台复杂度越高(如组件间通信、权限控制)。

106. 平时技术学习途径有哪些?

  • 官方文档:优先阅读框架/工具的官方文档(如Vue、React、MDN),保证知识准确性。
  • 技术社区:掘金、知乎、Stack Overflow、GitHub(看源码、issue讨论)、Medium(英文文章)。
  • 视频课程:极客时间、慕课网(系统学习专题,如“前端性能优化”“TypeScript实战”)。
  • 实践项目:复现经典功能(如手写Promise、虚拟DOM)、参与开源项目(修复bug、提PR)、开发个人项目(如博客系统)。
  • 交流分享:参加技术沙龙、公司内部分享、加入技术群讨论问题,倒逼自己梳理知识。

107. 开源社区做过什么贡献没? Vue、组件库有参与过没?issue 呢?

(根据实际情况回答,以下为示例)

  • 暂无代码贡献,但曾在Vue的GitHub仓库中针对“v-model在自定义组件中双向绑定失效”的场景提交过issue,附上复现代码和环境信息,后被官方确认是文档描述不清晰,更新文档后解决。
  • 在Element UI的社区中回答过其他开发者关于“表格动态列渲染”的问题,提供了基于v-ifslot的解决方案,被采纳为推荐答案。
  • 计划深入学习源码后,尝试修复简单bug(如组件样式兼容问题),逐步参与开源贡献。

108. 哪些是宏任务,哪些是微任务

  • 宏任务setTimeoutsetIntervalsetImmediate(Node)、I/O(文件读写)、UI渲染requestAnimationFrame
  • 微任务Promise.then/catch/finallyprocess.nextTick(Node,优先级高于其他微任务)、MutationObserver(监听DOM变化)。

109. 宏任务和微任务本质区别是什么?

  • 执行时机:微任务在当前同步代码执行完后、宏任务执行前立即执行(优先级更高);宏任务需等待微任务队列清空后才执行。
  • 来源:微任务多为JS引擎自身发起(如Promise回调),宏任务多为宿主环境发起(如浏览器的setTimeout、Node的I/O)。
  • 影响:微任务执行时可修改DOM,且在同一事件循环中生效;宏任务执行时可能伴随UI渲染(如setTimeout回调执行前可能触发一次渲染)。

110. 了解过浏览器的V8引擎吗

  • V8是Chrome和Node.js的JS引擎,核心功能是将JS代码编译为机器码执行(而非解释执行),提升运行效率。
  • 核心模块
    • 解析器(Parser):将JS代码转为抽象语法树(AST)。
    • 解释器(Ignition):将AST转为字节码,边解释边执行,启动快。
    • 编译器(TurboFan):监控热点代码(频繁执行的函数),将字节码编译为优化后的机器码,提升执行速度。
    • 垃圾回收器:回收不再使用的内存(如标记-清除算法处理老年代,Scavenge算法处理新生代)。

111. 目前业界流行的js引擎

  • V8:Google开发,用于Chrome、Node.js、Electron,性能优异,支持ES新特性快。
  • SpiderMonkey:Mozilla开发,用于Firefox,JS的第一个引擎,支持多线程编译。
  • JavaScriptCore:Apple开发,用于Safari、iOS浏览器,注重移动端性能优化。
  • Chakra:Microsoft开发,用于Edge(旧版),支持即时编译(JIT)。

112. 火狐采用什么引擎

火狐(Firefox)采用SpiderMonkey引擎,由Mozilla基金会开发,是最早的JavaScript引擎之一,支持ES标准和多线程编译,对动态类型优化较好。

113. csr、ssr、ssg的区别

类型全称渲染时机优点缺点适用场景
CSR客户端渲染浏览器前后端分离,交互流畅,服务器压力小首屏加载慢(需下载JS后渲染),SEO差管理系统、交互密集型应用
SSR服务端渲染服务器首屏快,SEO友好服务器压力大,开发复杂(需处理服务端状态)博客、电商详情页
SSG静态站点生成构建时首屏极快(纯静态文件),SEO好,成本低内容更新需重新构建文档站、营销页(内容固定)

114. ssr 适合哪种业务场景

  • 首屏加载速度要求高:如电商首页、活动页(用户等待时间直接影响转化率)。
  • SEO依赖强:如资讯网站、博客、商品详情页(搜索引擎更易抓取服务端渲染的HTML内容)。
  • 内容动态但更新不频繁:如新闻网站(可结合缓存机制,减少服务器压力)。
  • 不适合:交互密集型应用(如在线编辑器)、实时数据展示(如股票行情,SSR渲染成本高)。

115. Vue 对 SSR 的支持力度

  • Vue通过vue-server-renderer库支持SSR,提供完整的服务端渲染解决方案:
    • 核心:创建两个Vue实例(客户端实例+服务端实例),服务端渲染生成HTML字符串,客户端激活(hydrate)为可交互页面。
    • 生态:Nuxt.js(基于Vue的SSR框架)简化了配置,支持自动路由、异步数据(asyncData)、静态生成(SSG),降低开发门槛。
    • 限制:需处理服务端与客户端的状态同步(如避免服务端不存在的window对象),对第三方组件兼容性要求高(需支持SSR)。

116. 有写过Node代码经验吗

(根据实际情况回答)
例:有,用Node.js(Express框架)开发过接口服务:

  • 实现用户登录(JWT认证)、商品列表查询等接口,连接MySQL数据库(用Sequelize ORM)。
  • 开发中间件(如请求日志、错误处理),配置CORS跨域,部署到阿里云ECS。
  • 了解基本的Node特性(异步I/O、事件循环),但在高并发处理、内存优化方面经验较少,正在学习。

117. pnpm 相当于 yarn 的优势在哪里

  • 磁盘空间效率:pnpm通过硬链接+符号链接共享依赖包,同一版本的包只存储一次(yarn/npm可能重复存储),节省磁盘空间。
  • 安装速度:依赖复用减少下载,安装速度快于yarn(尤其项目多的场景)。
  • 依赖管理:严格的依赖树(node_modules结构与package.json声明一致),避免yarn/npm的“依赖提升”导致的版本冲突。
  • 安全性:默认禁止访问未声明的依赖(yarn/npm允许访问间接依赖),减少供应链攻击风险。

118. yarn在包处理上面是什么样的原理

  • 依赖解析:读取package.json,递归解析所有依赖(直接+间接),生成依赖树,检查版本冲突(遵循语义化版本)。
  • 安装策略
    • 扁平化依赖(hoisting):将依赖提升到node_modules根目录,减少嵌套层级,避免重复安装。
    • 缓存机制:下载的包缓存到本地(~/.yarn/cache),重复安装时直接复制,提升速度。
  • 锁文件:生成yarn.lock记录依赖的精确版本和下载地址,保证不同环境安装的依赖一致。

119. 动态更新引擎和微前端、微组件的本质区别?

  • 动态更新引擎:聚焦于“无需重新部署即可更新应用”,通过远程加载JS/CSS模块实现功能更新(如热更新、动态插件),不关注应用拆分。
  • 微前端:将大型应用拆分为独立的子应用(如不同团队开发的模块),子应用可独立部署、技术栈无关,通过容器应用整合,核心是“应用隔离与整合”。
  • 微组件:更细粒度的拆分,将UI组件拆分为可复用的独立模块(如按钮、表单),通过组件库或 registry 管理,核心是“组件复用与标准化”。

120. 为什么在浏览器端编译?

  • 场景:在线代码编辑器(如CodeSandbox)、低代码平台(实时预览用户配置)、动态主题(根据用户设置编译CSS)。
  • 原因
    • 实时性:用户输入后立即编译并展示结果,提升交互体验。
    • 个性化:根据用户设备/设置动态生成适配代码(如不同屏幕尺寸的CSS)。
    • 灵活性:避免预编译的局限性(如无法预知用户的动态配置)。
  • 缺点:增加浏览器性能消耗,需控制编译复杂度(如用WebAssembly加速)。

121. 私有化部署到客户机房,但是项目中有图片访问CDN资源,会不会出现访问不到的情况

  • 可能出现

    • 客户机房网络限制(如禁止访问公网CDN域名),导致图片加载失败。
    • CDN服务中断或域名变更,客户机房无法感知(私有化部署后难以实时更新图片地址)。
  • 解决方式

    • 图片本地化:将CDN图片下载到客户服务器,通过内网地址访问(需提前沟通图片存储需求)。
    • 配置可替换:项目中图片地址通过配置文件管理,部署时根据客户环境替换为内网地址或客户自建CDN。
    • 网络适配:与客户确认网络策略,开放CDN域名访问权限,或使用客户认可的内网CDN。

122. 有没有接触过因为响应式出现的的bug,业务场景,怎么解决的

  • 场景:Vue 2中,对数组使用索引修改元素(this.arr[0] = 1),视图不更新。

  • 原因:Vue 2的响应式系统通过Object.defineProperty实现,无法监听数组索引的变化(设计时为了性能,只监听了push/pop等方法)。

  • 解决

    • Vue.set(或this.$set)强制触发响应式:this.$set(this.arr, 0, 1)
    • 调用数组变异方法(如splice):this.arr.splice(0, 1, 1)
    • 升级到Vue 3(用Proxy代理数组,支持索引修改的响应式)。

真题集锦

生命周期
小程序生命周期
路由守卫
小程序不同页面传参
你做的项目
数组方法
现场给了一个题,说执行顺序

介绍一下你最近一个项目的业务流程,你负责哪些事情
权限
双签怎么做的
埋点怎么做的
小程序登录怎么做的
懒加载怎么做的
敏感信息怎么处理
多端开发怎么做的
怎么判断一个请求是h5端发起的,还是pc端发起的
你的优势是什么
与后端发生冲突怎么做

内边距,外边距是什么
前端怎么申明变量,区别
前端有哪些异步函数,怎么处理异步
你知道回掉地狱吗,有什么危害,怎么解决
怎么防止sql注入
怎么防止xss,ddos网络攻击
怎么处理大规模的并发,你之前项目中有遇到吗,讲讲场景,怎么解决的
有做过票务系统吗
有做过电商系统吗,讲讲你做的
is,ts区别
前端如何处理敏感信息
数据安全你做过哪些
有做过小程序吗
有没有做过html转译
antd有用过吗,与其他框架比感觉怎么样
前端崩溃怎么办,如何优化

diff算法
闭包
es6
双向数据绑定原理
插槽,项目中怎么用的
vuex,项目中怎么用的
懒加载有哪几种方式,具体实现
h5,移动端pc端如何适配
封装过哪些组件,考虑的点是什么
自定义指令
父子组件生命周期执行顺序
nexttick怎么用,实现原理,返回一个什么,源
码看过吗
promise(深)
async await与promise区别
axios怎么封装,如何取消请求
同源策略
cookie怎么在跨域之间共享
解决跨域的方式
项目优化
is数据类型,检测数据类型的方式
数组遍历的方法,map foreach区别
前端怎么批量处理请求并发

讲讲你印象最深刻的项目,你负责什么
面向对象,面向过程
vue是面向对象还是过程,为什么
vue页面开发是面向对象还是过程,为什么前端发请求到后端整个过程,越详细越好
前端是如何精确找到后端的,原理是什么
有没有做过项目部署
你的代理是怎么做的
chorm浏览器有哪些东西,都有什么作用一个页面有50个组件,某个组件卡了,怎么精确定位,怎么解决
优化做过哪些
响应式是什么,原理是什么,有哪些副作用有没有接触过因为响应式出现的的bug,业务场景,怎么解决的
项目的亮点,难点,怎么解决的

介绍一下你做的项目
js数据类型
es6
原型,原型链
继承,实现继承的方式
class
垃圾回收机制
evenloop
闭包
mvvm
数据绑定原理
v2 和v3区别
vuex
插槽,作用域插槽是用来干嘛的
路由守卫,用来干嘛
有没有对请求做过处理
webpack打包,有没有做过改动或者优化
cdn为什么没有跨域
如何知道一个dom在不在可是区域内
权限怎么做的
大文件上传如何实现
pdf渲染怎么做的
你会什么,你擅长什么,你有哪些不足,如何与客户去沟通,当需求频繁变更,你如何处理,当你感觉客户需求不对的时候该怎么办当你工作出错你该怎么办,有没有带过团队,能不能带团队,你之前项目多少人,你负责什人…

ts中typeof和instance区别
v3组件传值
改变this指向的方式
data为啥是个函数
大文件上传
v2和v3区别
说一下vuex
存储方式
懒加载,缓存,代码压缩等优化方案
vmodel原理,如何手写一个vmodel
数据双向绑定原理
生命周期
promise
口述深拷贝,深浅拷贝区别
es6
set干嘛的数组方法,
webpack
项目亮,难点
换肤实现方案,webpack实现tree shanking
sea和require的区别

1.vue2和vue3的区别
2.js 排序
3.js 数组去重
4.ref reactive
5.vue2和vue3生命周期的区别,删了什么
6.await 和settimeout 宏任务和微任务
7.数据类型
8.=的区别
9.封装el-dialog,暴露方法,绑定属性
10.封装axios
11.深拷贝
12.项目性能优化

TS:any vs ever
TS:type vs interface
TS:泛型
v3与v2的区别有哪些
插槽
webpack的配置和vite的区别
组件通信
自定义指令和场景
输入url到页面呈现发生了什么
axios接口封装,如何处理轮循结果先请求后返回问题
qiankun不同应用间的操作痕迹缓存前端页面优化有哪些

二面
样式水平居中
如何重写组件库样式
微前端避免外面的壳和里面的子应用有样式冲突
实际写的比较多的ts类型体操
哪些是宏任务,哪些是微任务
宏任务和微任务本质区别是什么?
了解过浏览器的V8引擎吗
目前业界流行的js引擎
火狐采用什么引擎
csr、ssr、ssg的区别
ssr 适合哪种业务场景
Vue 对 SSR 的支持力度
有写过Node代码经验吗
pnpm 相当于 yarn 的优势在哪里
yarn在包处理上面是什么样的原理
动态更新引擎和微前端、微组件的本质区别?
为什么在浏览器端编译?
私有化部署到客户机房,但是项目中有图片访问CDN资源,会不会出现访问不到的情况
虚拟列表怎么实现的?
开源社区有方案吗?为什么不直接用
门户所有的页面是如何配置出来的?能力边界在哪里?
代码题:手写 todo list 支持将状态在已完成和待完成之间切换
平时技术学习途径有哪些?
开源社区做过什么贡献没?
Vue、组件库有参与过没?issue 呢?

二、面试题原理重点频次排序

以下是按高频到低频排序的面试题列表。原理的频次基于题目覆盖的核心概念重复出现次数和重要性综合评估:

高频原理(出现 5+ 次)

1、生命周期(组件化核心)
  • vue生命周期
  • 小程序生命周期
  • 父子组件生命周期执行顺序
2、异步编程与事件循环(JS基础)
  • Promise(深)
  • async/await与Promise区别
  • 前端异步函数及处理方式
  • 回调地狱的危害与解决
  • 前端批量处理并发请求
3、Vue框架核心原理(响应式与组件化)
  • 双向数据绑定原理
  • Vue2和V3区别
  • Vuex及项目中应用
  • 插槽/作用域插槽
  • 数据响应式原理及副作用
4、性能优化(工程化重点)
  • 懒加载实现方式
  • 项目优化(代码压缩/CDN/缓存)
  • 大文件上传方案
  • 前端崩溃监控与优化
  • Diff算法
5、安全防护(安全必问)
  • 防止XSS/CSRF/DDoS
  • 敏感信息处理
  • 数据安全措施

中高频原理(出现 3-4 次)

1、JavaScript核心概念(语言基础)
  • 闭包
  • 原型链与继承
  • ES6特性(Set/箭头函数等)
  • 数据类型检测(is检测)
2、组件通信与状态管理
  • 小程序不同页面传参
  • V3组件传值
  • 自定义指令
  • 前端变量声明方式(var/let/const)
3、工程化与工具
  • Webpack打包优化
  • Tree Shaking实现
  • 代理配置(跨域解决)
4、网络与HTTP
  • 跨域解决方案
  • Cookie跨域共享
  • 请求封装(Axios取消请求)
5、项目经验(综合能力)
  • 项目业务流程与职责
  • 项目亮点/难点解决
  • 权限系统设计

中低频原理(出现 1-2 次)

1、多端开发与适配
  • H5/PC/移动端适配方案
  • 多端开发框架(如Uniapp)
2、浏览器原理
  • 同源策略
  • 事件循环(EventLoop)
3、其他框架/工具
  • Antd与其他框架对比
  • HTML转义(防XSS)
  • PDF渲染方案
4、算法与数据结构
  • 深拷贝手写实现
  • 数组遍历方法(map/forEach区别)
5、团队协作与软技能
  • 与后端冲突处理
  • 需求变更应对
  • 项目部署经验

排序依据说明

  • 高频原理覆盖了前端核心能力:组件化(生命周期)、异步编程(Promise)、框架原理(Vue响应式)、性能优化、安全防护,是面试必问点。
  • 中高频原理涉及语言基础(闭包/原型链)、工程化(Webpack)、网络(跨域),是进阶能力的体现。
  • 低频题如“票务系统经验”或“团队人数”属于业务或场景题,依赖候选人背景,频次较低。

此排序可帮助候选人优先掌握高频核心原理,再逐步扩展到中低频领域。

三、部分面试题答案解析

1、css中如何实现水平垂直居中

方法一:flex:
display: flex;
justify-content: center; 
align-item: center;
方法二:绝对定位+margin:auto:
position: absolute; 
left: 0;
right: 0;
top: 0;
bottom: 0;
margin:auto;
方法三:已知子元素宽高(200*200) + 绝对定位 + 负margin
position: absolute;
top: 50%;
left: 50%;
margin-top: -100px;
margin-left: -100px;
方法四:已知子元素宽高(200*200) + 绝对定位 + calc
position: absolute;
top: calc(50% - 100px);
left: calc(50% - 100px);
方法五:绝对定位 + transform
position: absolute;
left: 50%; (定位父元素的50%)
top: 50%;
transform: translate(-50%, -50%); (自己的50%)
方法六:table-cell实现
display: table-cell;
text-align: center;
vertical-align: center;
方法七:Grid
display: grid;
justify-self: center;
align-self: center;

2、数组相关API有哪些

  • 尾巴新增/删除元素:push()pop()
  • 正序/倒序:sort()reverse()
  • 前面删除/新增元素:shift()unshift()
  • 数组截取 - 返回数组的指定部分,创建一个新的数组:slice()
  • 数组修改 - 通过删除、替换或添加元素的方式修改数组,并返回被删除的元素:splice()
  • 数组拼接 / 数组指定的分隔符拼接转为字符串 / 求和 :concat()join()reduce()
  • 遍历:forEach()map()
  • 数组筛查 / 数组填充 :filter()fill()
  • 数组过滤 / 判断数组中是否包含:find()include()
  • 数组查找定位:indexOf()findIndex()lastIndexOf()
  • 转字符串:toString()toLocaleString()

3、数组去重

例:将下面数组去除重复元素(以多种数据类型为例)
const arr = [1, 2, 2, 'abc', 'abc', true, true, false, false, undefined, undefined, NaN, NaN]
方法一:利用Set()+Array.from()
const result = Array.from(new Set(arr))
console.log(result) // [ 1, 2, 'abc', true, false, undefined, NaN ]
方法二:新数组 + forEach + indexof / includes
  • indexof 无法检测 NaN
  • includes 支持全类型:因为includes 在进行判断是否包含某元素时会调用sameValueZero方法进行比较,如果为NaN,则会使用isNaN()进行转化
function removeDuplicate(arr) {
  const newArr = []
  arr.forEach(item => {
    if (newArr.indexOf(item) === -1) {
    // if (!newArr.includes(item)) {
      newArr.push(item)
    }
  })
  return newArr // 返回一个新数组
}

const result = removeDuplicate(arr)
console.log(result) 
// 使用indexOf:[ 1, 2, 'abc', true, false, undefined, NaN, NaN ]
// 使用includes:// [ 1, 2, 'abc', true, false, undefined, NaN ]
方法三:两层循环 + splice
  • 通过两层循环对数组元素进行逐一比较,然后通过splice方法来删除重复的元素。
  • 无法检测 NaN :此方法对NaN是无法进行去重的,因为进行比较时NaN !== NaN。
function removeDuplicate(arr) {
  let len = arr.length
  for (let i = 0; i < len; i++) {
    for (let j = i + 1; j < len; j++) {
      if (arr[i] === arr[j]) {
        arr.splice(j, 1)
        len-- // 减少循环次数提高性能
        j-- // 保证j的值自加后不变
      }
    }
  }
  return arr
}

const result = removeDuplicate(arr)
console.log(result) // [ 1, 2, 'abc', true, false, undefined, NaN, NaN ]
方法四:filter() + indexOf()
  • filter方法会对满足条件的元素存放到一个新数组中,结合indexOf方法进行判断。
  • 无法检测 NaN
function removeDuplicate(arr) {
  return arr.filter((item, index) => {
    return arr.indexOf(item) === index
  })
}

const result = removeDuplicate(arr)
console.log(result) // [ 1, 2, 'abc', true, false, undefined ]

注意:这里的输出结果中不包含NaN,是因为indexOf()无法对NaN进行判断,即arr.indexOf(item) === index返回结果为false。

方法五:map() + has() + set()

Map对象是JavaScript提供的一种数据结构,结构为键值对形式,将数组元素作为map的键存入,然后结合has()和set()方法判断键是否重复。

function removeDuplicate(arr) {
  const map = new Map()
  const newArr = []

  arr.forEach(item => {
    if (!map.has(item)) { // has()用于判断map是否包为item的属性值
      map.set(item, true) // 使用set()将item设置到map中,并设置其属性值为true
      newArr.push(item)
    }
  })

  return newArr
}

const result = removeDuplicate(arr)
console.log(result) // [ 1, 2, 'abc', true, false, undefined, NaN ]

注意:使用Map()也可对NaN去重,原因是Map进行判断时认为NaN是与NaN相等的,剩下所有其它的值是根据 === 运算符的结果判断是否相等。

方法六:利用对象

利用了对象的属性名不可重复这一特性

function removeDuplicate(arr) {
  const newArr = []
  const obj = {}

  arr.forEach(item => {
    if (!obj[item]) {
      newArr.push(item)
      obj[item] = true
    }
  })

  return newArr
}

const result = removeDuplicate(arr)
console.log(result) // [ 1, 2, 'abc', true, false, undefined, NaN ]

4、Promise相关方法以及对Promise的一些了解

  • 异步、代理、设计模式
  • 基于 Promise 可以有效管理JS中的异步编程
  • 解决传统异步编程 + 回调函数导致的“回调地狱”问题
  • 状态:初始状态(pending)、兑现成功(fulfilled、resolved)、操作失败(rejected
  • 常用API:Promise.all()Promise.any()Promise.race()Promise.resolve()Promise.reject()Promise.prototype.then()Promise.prototype.fecth()Promise.prototype.finally()

5、如果Promise只要一个成功就成功的方法

Promise.any()

6、Vue3对Vue2的优化

在这里插入图片描述

7、Vue源码碰过吗?关于响应式的原理知道吗

8、vue 3 的响应式是如何实现的?

  • 总:
    vue3 的响应式实现主要有两个部分:reactiveref
  • 分:

reactive 主要是通过 proxy 进行的响应式实现,核心是监听复杂数据类型gettersetter 行为。当监听到 getter 行为的时候那么就收集当前的依赖行为,也就是 effect 。 当触发 setter 行为的时候,那么就触发刚才收集的依赖。那么此时,所有获取到当前数据的地方都会更新执行,也就是完成了响应性。
但是 proxy 只能监听复杂数据类型,没有办法监听简单数据类型。所以 vue 专门提供了 ref 方法。 ref 方法既可以处理简单数据类型、也可以处理复杂数据类型。 它的实现在 3.2 之前和 3.2 之后是不同的。

  • 3,2 之前主要通过 Object.defineProperty 进行实现,
  • 在 3.2 版本的时候,根据社区贡献改为了 get valueset value 标记的方式进行实现。

这也是为什么 ref 类型的数据必须要通过 .value 的方式使用的原因(本质上是触发 value 方法)。
ref 接收复杂数据类型的时候,会直接通过 toReactive 方法,把复杂数据类型交给 reactive 进行处理。

  • 总:
    整个的 vue3 响应性,主要就是由这两大块来进行实现的。 proxy 处理复杂数据类型,get valueset value 处理简单数据类型。核心都是监听 setter 和 getter ,然后触发 effect 的方式

8、事件循环

9、npm install之后发生了什么?

在这里插入图片描述

  • 首先,npm install 需要检查是否有附加的命令参数,如-g--save--saved-dev,以决定依赖类型(全局、生产依赖、开发依赖)。如果没有指定,则之后会安装 package.json 中列出的所有依赖。
  • 接着,npm install 会按优先级查找 配置文件项目级.npmrc > 用户级 .npmrc > 全局级 .npmrc > npm 内置 .npmrc,并根据配置调整安装行为。
  • 如果项目定义了 preinstall 钩子(例如:npm run preinstall),它会在依赖安装前被执行。可以在此步骤进行一些初始化操作,如检查版本、清理缓存等。
  • 然后检查是否有lock文件,有的话会检查package.json中的依赖版本是否和package-lock.json中的依赖有冲突。如果没有冲突,直接在缓存中查找包信息。
    如果没有lock文件,会先npm远程仓库去获取包信息,之后根据package.json构建依赖树,具体过程:
    a、构建 依赖树时,不管其是直接依赖还是子依赖的依赖,优先将其放置在 node_modules 目录
    b、当遇到相同模块时,判断已放置在依赖树的模块版本是否符合新模块的版本范围,如果符合则跳过,不符合则在 当前模块的 node_modules 下放置该模块。
  • 之后再在 缓存 中依次查找依赖树的每个包:
    a、不存在缓存:从npm远程仓库下载包,检验包的完整性,检验不通过就重新下载,检验通过会将下载的包复制到npm缓存目录并按照 扁平化 的依赖结构解压到node-modules中
    b、存在依赖:将缓存按照扁平化的依赖结构解压到node-modules
  • 生成lock文件

10、npm常用命令

npm install moduleName # 安装模块到项目目录
npm install -g moduleName # -g 意思是将模块安装到全局,具体安装到磁盘哪个位置,要看 npm config prefix 的位置。
npm install --save moduleName # --save 的意思是将模块安装到项目目录下,并在package文件的dependencies节点写入依赖。
npm install --save-dev moduleName # --save-dev 的意思是将模块安装到项目目录下,并在package文件的devDependencies节点写入依赖。

11、理解CI/CD(持续集成/持续部署)技术的意义,至少熟练掌握一种CI/CD工具的使用

a、 持续集成与持续部署的区别是什么?

CI持续集成,代码合并提交,主要关注开发阶段,确保每次代码提交后都能通过自动化构建和测试,从而尽早发现并修复问题
CD持续部署,不仅将代码集成到主分支,还会自动将经过测试的代码直接部署到生产环境,实现快速迭代和反馈

b、为什么CI/CD对企业如此重要?它的作用价值?
  • 提升代码质量:每次代码提交后都会触发自动化测试,及时发现并修复问题,降低代码缺陷率
  • 加速产品交付速度:实现了每日多次部署,极大地提升了用户体验和客户满意度。
  • 促进团队协作:减少了沟通成本,提高了工作效率
  • 增强风险控制能力:通过自动化测试和监控,确保每一个版本都是安全可靠的,故障恢复时间大幅缩短。

12、说一下 v-if 和 v-show 的区别,以及它们的使用场景。

  • 总:
    v-ifv-show 都是用来控制元素是否展示的。但是它们的实现原理和在项目中的应用场景确实有一定区别
  • 分:

那么咱们先说实现原理

  • v-show 主要通过 css 属性 diplay:none; 来控制元素的显示和隐藏。那么虽然用户看不到,但是 dom 依然是存在的
  • v-if 主要通过 是否渲染 来控制元素的展示和消失。当用户看不到它的时候,那么这个元素是不存在的

这样一个存在和不存在的特性,如果是在企业项目中差别就会比较大了。特别是在组件上使用时,其中会涉及到 生命周期 以及 状态保存 的问题。

比如,通过一个 dialog 来处理编辑用户功能。

  • 那么使用 v-show 时。当页面展示时,dialog 就已经被渲染出来了。那么这就意味着 dialog 的生命周期已经完成,如果涉及到 props 传参,那么其实参数就已经传递了。同时当修改了 dialog 的数据,关闭 dialog 时,那么 dialog 的状态会被保持
  • 而使用 v-if 的话。当页面展示时,dialog 是没有被渲染的。只有主动触发绑定值变化,dialog 才会显示出来。这就意味着,此时 dialog 的生命周期会执行,同时 props 才开始传递。同时当修改了 dialog 的数据,关闭 dialog 时,那么 dialog 的状态会被清空(组件消失)

v-show 的性能相对会更高,而 v-if 会更加消耗性能
但是很多场景中,性能的消耗对于用户而言,感知是有限的。

  • 总:

所以,v-if 和 v-show 的选择,我们需要基于当前 dialog 的业务场景来进行判断

  • 如果当前 dialog 需要保存状态 或者 需要进行初始渲染,那么可以使用 v-show
  • 如果当前 dialog 不需要保存状态 或者 希望控制它的渲染时机,那么可以使用 v-if。

13、有没有做过review,你们注释怎么写的

是的,我有过参与代码审查的经验。在代码审查中,我的注释写得较为清晰和规范,主要体现了以下几个特点:

代码审查的步骤:
  • 理解代码背景
    在审查之前,我会先了解代码的上下文,包括项目的需求、已有的代码结构和功能。
  • 检查代码质量
    可读性:确保代码遵循清晰的命名 conventions 和编码风格。
    错误率:寻找代码中的语法错误、逻辑错误或不规范的代码行为。
    可维护性:检查代码是否易于理解和维护,是否有冗余或重复的代码。
  • 提出反馈
    在发现问题后,我会用简洁明了的语言提出具体的建议和修改意见,例如:
    修改不规范的代码行为。
    添加注释解释复杂的逻辑或功能。
    删除冗余的代码。
在这里插入代码片
在这里插入代码片
注释写法
  • 功能注释:用于解释函数或方法的用途
  • 逻辑注释:用于解释复杂的逻辑或算法
  • 错误处理注释:用于说明异常处理或错误处理的逻辑
  • 性能优化注释:用于说明优化后的逻辑
// 原始代码可能有性能问题,已进行优化
function optimizedCode() {
   // 优化后的逻辑
} 

14、SSE 和 WebSocket 的区别,差异对比

通信模型

  • SSE单向通信模型,只能由服务器向客户端推送数据
  • WebSocket双向通信模型,客户端和服务器可以互相发送消息。

连接性

  • SSE使用长轮询HTTP流技术,SSE需要频繁地发起HTTP请求来获取数据
  • WebSocket使用持久连接。WebSocket只需在握手阶段建立一次连接,然后保持连接打开。

实时性

  • WebSocket提供了更低的延迟和更高的实时性,因为它支持双向通信,可以立即将数据推送给客户端。
  • SSE虽然也可以实现实时性,但由于其单向通信模型,需要服务器定期发送数据,实时性比较低

浏览器支持

  • WebSocket在现代浏览器中得到广泛支持,包括Chrome、Firefox、Safari等。
  • SSE在大多数现代浏览器中也有支持,但在某些旧版本浏览器中可能存在兼容性问题。

API复杂性

  • WebSocket提供了更灵活和复杂的API,可以处理更高级的通信需求。
  • SSE相对简单,使用浏览器的原生 EventSource 接口即可。

适用场景

  • SSE是一种基于HTTP的单向通信机制,适用于实时更新数据的应用场景,如视频流、用户活动更新等;
  • WebSocket是一种全双工通信协议,支持双向通信,适用于实时互动的应用场景,比如构建实时聊天应用、游戏客户端、物联网设备等

15、JS 中如何实现大对象深度对比

在 JavaScript 里,要实现大对象的深度对比,可递归检查对象的每个属性。核心在于处理不同数据类型,像基本类型、对象、数组,还有循环引用等情况。以下是实现思路:

function deepEqual(obj1, obj2, cache = new WeakSet()) {
  // 基本类型值的直接比较
  if (obj1 === obj2) {
    return true;
  }

  // 处理 null 和非对象类型
  if (obj1 === null || typeof obj1 !== 'object' ||
      obj2 === null || typeof obj2 !== 'object') {
    return false;
  }

  // 检查循环引用
  if (cache.has(obj1) || cache.has(obj2)) {
    return true;
  }
  cache.add(obj1);
  cache.add(obj2);

  // 处理特殊对象类型
  if (obj1 instanceof Date && obj2 instanceof Date) {
    return obj1.getTime() === obj2.getTime();
  }
  if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
    return obj1.toString() === obj2.toString();
  }

  // 获取所有属性键,包括 Symbol 类型
  const keys1 = [...Object.getOwnPropertyNames(obj1), ...Object.getOwnPropertySymbols(obj1)];
  const keys2 = [...Object.getOwnPropertyNames(obj2), ...Object.getOwnPropertySymbols(obj2)];

  if (keys1.length !== keys2.length) {
    return false;
  }

  // 递归比较每个属性
  return keys1.every(key => deepEqual(obj1[key], obj2[key], cache));
}

16、如何理解数据驱动视图,有哪些核心要素

数据驱动视图是现代前端框架(如 Vue、React)的基础理念。其核心是视图会依据数据的变化自动更新,开发者只需专注于数据的流转,而无需手动操作 DOM。
核心要素如下

  • 响应式系统:当数据发生改变时,能够自动检测到并发出通知。
  • 虚拟 DOM:借助轻量级的 JavaScript 对象来映射真实 DOM,从而提升更新效率。
  • 渲染函数:把数据转换为虚拟 DOM 或者直接生成 DOM。
  • Diff 算法:找出新旧虚拟 DOM 之间的差异,只更新需要变化的部分。
  • 生命周期钩子:在数据变化到视图更新的过程中提供干预点。

17、vue-cli 都做了哪些事儿,有哪些功能

vue-cli 是 Vue.js 官方提供的项目脚手架工具,主要功能如下:

  • 项目初始化:按照预设模板快速搭建项目结构,像单页应用、多页应用等。
  • 配置封装:隐藏 webpack、Babel 等工具的复杂配置,提供简洁的配置接口。
  • 开发服务器:支持热更新、错误 overlay 等功能的本地开发服务器。
  • 构建优化:对生产环境的代码进行优化,包括代码分割、Tree-shaking 等。
  • 插件系统:可以通过插件扩展项目功能,如添加 TypeScript、路由、状态管理等。
  • UI 界面:提供图形化界面来创建和管理项目。

18、JS 执行 100 万个任务,如何保证浏览器不卡顿

若要执行大量任务又不阻塞浏览器的主线程,可以采用以下策略:

  • Web Workers:把计算密集型任务放到 Web Workers 中执行,避免阻塞 UI 渲染。
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ tasks: bigArrayOfTasks });
worker.onmessage = (e) => {
  console.log('All tasks completed', e.data);
};

// worker.js
self.onmessage = (e) => {
  const results = e.data.tasks.map(task => processTask(task));
  self.postMessage(results);
};
  • 任务分片:将大任务拆分成小任务,利用 requestAnimationFramesetTimeout 分批执行。
function processTasksInChunks(tasks, chunkSize = 100) {
  const results = [];
  let index = 0;

  function processChunk() {
    const chunk = tasks.slice(index, index + chunkSize);
    results.push(...chunk.map(processTask));
    index += chunkSize;

    if (index < tasks.length) {
      requestAnimationFrame(processChunk);
    } else {
      console.log('All tasks completed', results);
    }
  }

  processChunk();
}
  • Generator 函数:利用 Generator 函数暂停和恢复任务的执行。
function* taskGenerator(tasks) {
  for (const task of tasks) {
    yield processTask(task);
  }
}

const gen = taskGenerator(bigArrayOfTasks);
function runGenerator() {
  for (let i = 0; i < 100; i++) {
    const { done, value } = gen.next();
    if (done) return;
    // 处理 value
  }
  requestAnimationFrame(runGenerator);
}
runGenerator();

19、 JS 放在 head 里和放在 body 里有什么区别

  • 放在 head 标签中
    • 会在 HTML 解析之前加载和执行 JS 文件。
    • 若 JS 文件较大或者依赖网络,会导致页面渲染延迟,出现白屏现象。
    • 若要操作 DOM,需要确保 DOM 已经加载完成(可使用 DOMContentLoaded 事件)。
  • 放在 body 标签末尾
    • 先加载和解析 HTML,再加载和执行 JS。
    • 能避免阻塞页面渲染,提升用户体验。
    • 可以直接访问 DOM 元素,无需额外的加载事件。
  • 优化建议
    • 使用 deferasync 属性加载外部 JS 文件。
    • 对于不操作 DOM 的 JS 文件,可放在 head 标签中并使用 async
    • 对于操作 DOM 的 JS 文件,建议放在 body 标签末尾。

20、Eslint 代码检查的过程是什么

ESLint 代码检查的流程如下:

  1. 配置加载:读取 ESLint 配置文件(.eslintrc),确定规则、解析器等。
  2. 代码解析:借助解析器(如 Babel-ESLint)将代码转换为抽象语法树(AST)。
  3. 规则应用:遍历 AST,应用配置好的规则进行检查,每个规则对应一个或多个 AST 节点类型。
  4. 问题收集:收集不符合规则的代码位置和具体信息。
  5. 报告生成:将发现的问题以可读的格式输出,如命令行格式、JSON 等。
  6. 自动修复:对于支持自动修复的规则,直接修改源文件。

21、JS 实现一个虚拟滚动加载

虚拟滚动是一种仅渲染可视区域内容的技术,适用于大数据列表。下面是一个简单的实现:

class VirtualScroll {
  constructor(container, itemHeight, data) {
    this.container = container;
    this.itemHeight = itemHeight;
    this.data = data;
    this.visibleCount = Math.ceil(container.clientHeight / itemHeight);
    this.startIndex = 0;
    this.endIndex = this.visibleCount;

    this.init();
  }

  init() {
    // 创建可视区域容器
    this.visibleContainer = document.createElement('div');
    this.visibleContainer.style.position = 'relative';
    this.visibleContainer.style.overflow = 'hidden';
    this.visibleContainer.style.height = '100%';
    this.container.appendChild(this.visibleContainer);

    // 创建内容包装器
    this.contentWrapper = document.createElement('div');
    this.contentWrapper.style.position = 'absolute';
    this.contentWrapper.style.top = '0';
    this.contentWrapper.style.left = '0';
    this.contentWrapper.style.width = '100%';
    this.visibleContainer.appendChild(this.contentWrapper);

    // 设置总高度以显示滚动条
    this.container.style.overflow = 'auto';
    this.container.style.height = '300px';
    this.updateTotalHeight();

    // 初始渲染
    this.renderVisibleItems();

    // 监听滚动事件
    this.container.addEventListener('scroll', this.handleScroll.bind(this));
  }

  updateTotalHeight() {
    this.contentWrapper.style.height = `${this.data.length * this.itemHeight}px`;
  }

  renderVisibleItems() {
    // 清空当前内容
    this.contentWrapper.innerHTML = '';

    // 渲染可见项
    for (let i = this.startIndex; i < this.endIndex && i < this.data.length; i++) {
      const item = document.createElement('div');
      item.style.height = `${this.itemHeight}px`;
      item.style.padding = '8px';
      item.style.borderBottom = '1px solid #eee';
      item.textContent = `Item ${i}: ${this.data[i]}`;
      item.style.transform = `translateY(${i * this.itemHeight}px)`;
      this.contentWrapper.appendChild(item);
    }
  }

  handleScroll() {
    const scrollTop = this.container.scrollTop;
    const newStartIndex = Math.floor(scrollTop / this.itemHeight);
    const newEndIndex = newStartIndex + this.visibleCount;

    // 只有当起始索引或结束索引发生变化时才重新渲染
    if (newStartIndex !== this.startIndex || newEndIndex !== this.endIndex) {
      this.startIndex = newStartIndex;
      this.endIndex = newEndIndex;
      this.renderVisibleItems();
    }
  }
}

// 使用示例
const container = document.getElementById('virtual-scroll-container');
const data = Array.from({ length: 10000 }, (_, i) => `Data ${i}`);
new VirtualScroll(container, 30, data);

22、react-router 和原生路由的区别

  • react-router(SPA 路由)
    • 属于客户端路由,路由变化不会向服务器发送请求。
    • 通过监听 URL 的变化(如 hash 或 history API),动态渲染组件。
    • 应用场景包括单页应用(如社交网站、管理后台)。
    • 优点是切换速度快,用户体验好;缺点是初次加载时间长,SEO 支持较差。
  • 原生路由(传统路由)
    • 是服务器端路由,每次 URL 变化都会请求服务器,服务器返回新的 HTML 页面。
    • 应用场景包括多页应用(如新闻网站、博客)。
    • 优点是 SEO 友好,首屏加载快;缺点是页面切换有延迟,用户体验较差。

23、html 的行内元素和块级元素的区别

  • 块级元素(Block-level Elements)
    • 会独占一行,默认宽度是父元素的 100%。
    • 可以包含行内元素和其他块级元素。
    • 常见的块级元素有 <div><p><h1>-<h6><ul><form> 等。
  • 行内元素(Inline Elements)
    • 不会换行,宽度由内容决定。
    • 只能包含文本和其他行内元素。
    • 常见的行内元素有 <a><span><img><input><button> 等。
  • 主要区别
    • 布局方式:块级元素会换行,行内元素不会。
    • 宽度和高度:块级元素可以设置宽度和高度,行内元素通常由内容决定。
    • 内外边距:块级元素的内外边距会影响其他元素,行内元素的垂直边距可能不会生效。

23、介绍一下 requestIdleCallback API

requestIdleCallback 是浏览器提供的一个 API,它允许开发者在浏览器空闲时段执行低优先级任务,避免阻塞关键的渲染和用户交互。

  • 基本用法
requestIdleCallback((deadline) => {
  // deadline.timeRemaining() 返回当前空闲时段剩余的毫秒数
  while (deadline.timeRemaining() > 0 && tasks.length > 0) {
    const task = tasks.pop();
    processTask(task);
  }

  // 如果还有任务没完成,继续请求下一个空闲时段
  if (tasks.length > 0) {
    requestIdleCallback(handleTasks);
  }
});
  • 参数说明
    • timeout:设置任务的最大延迟时间,若超过这个时间,任务会在下次重排前执行。
    • deadline.timeRemaining():返回当前空闲时段剩余的时间。
    • deadline.didTimeout:表示任务是否因为超时才被执行。
  • 应用场景
    • 执行非关键的后台任务,如数据同步、缓存清理。
    • 预加载次要资源,如图像、字体。
    • 收集性能指标。

24、documentFragment API 是什么,有哪些使用场景

DocumentFragment 是一个轻量级的 DOM 对象,它可以作为临时容器来存储 DOM 节点,在将其插入真实 DOM 之前进行批量操作,从而减少重排和重绘,提高性能。

  • 基本用法
// 创建 DocumentFragment
const fragment = document.createDocumentFragment();

// 创建多个 DOM 节点
for (let i = 0; i < 1000; i++) {
  const div = document.createElement('div');
  div.textContent = `Item ${i}`;
  fragment.appendChild(div);
}

// 一次性将所有节点添加到 DOM 中
document.body.appendChild(fragment);
  • 使用场景
    • 批量添加或删除节点,减少 DOM 操作次数。
    • 与虚拟滚动结合使用,在片段中构建可见项,然后一次性添加到 DOM。
    • 动态构建复杂的 UI 组件,在片段中组装完成后再插入页面。

25、git pull 和 git fetch 有啥区别

  • git fetch
    • 从远程仓库下载最新的提交、分支等信息到本地仓库。
    • 不会自动合并到当前分支,需要手动执行 git mergegit rebase
    • 适合在合并前查看远程更新,避免意外合并。
  • git pull
    • 相当于 git fetchgit merge FETCH_HEAD 的组合操作。
    • 从远程仓库获取更新并直接合并到当前分支。
    • 可能会产生合并冲突,需要手动解决。
  • 建议工作流程
    • 执行 git fetch 获取远程更新。
    • 查看更新内容(如使用 git log)。
    • 执行 git mergegit rebase 合并到本地分支。

26、前端如何做页面主题色切换

实现页面主题色切换主要有以下几种方法:

  • CSS 变量
    /* 定义主题变量 */
    :root {
      --primary-color: #165DFF;
      --text-color: #333;
    }
    
    /* 使用变量 */
    .button {
      background-color: var(--primary-color);
      color: white;
    }
    
    /* 切换主题 */
    .theme-dark {
      --primary-color: #303133;
      --text-color: #fff;
    }
    
    // 切换主题的 JavaScript 代码
    function switchTheme(theme) {
      document.documentElement.className = theme;
    }
    
  • CSS Modules/SCSS
    • 为不同主题创建独立的 CSS 文件。
    • 通过动态加载不同的 CSS 文件来切换主题。
  • CSS-in-JS
    • 在 JavaScript 中定义样式,根据主题变量生成不同的样式。
    • 如 styled-components、emotion 等库的使用。
  • 工具库
    • 使用工具库如 theme-change 可以自动处理主题色的渐变过渡。

27、前端视角如何保证系统稳定性

从前端角度保证系统稳定性,可以采取以下措施:

  • 错误监控
    • 使用 Sentry、TrackJS 等工具捕获和记录运行时错误。
    • 实现全局错误处理(window.onerrorunhandledrejection)。
  • 降级处理
    • 对关键功能提供降级方案,如图片加载失败时显示默认图。
    • 使用 Feature Detection 检测浏览器特性,提供兼容方案。
  • 性能优化
    • 对代码进行分割和懒加载,避免首屏加载过多资源。
    • 实现虚拟滚动处理大数据列表。
    • 优化图片资源,使用合适的格式和尺寸。
  • 异常处理
    • 为异步操作添加错误捕获(.catch()try/catch)。
    • 实现超时控制,避免长时间无响应。
  • 容灾设计
    • 使用本地缓存(如 localStorage)在网络异常时提供离线体验。
    • 对关键 API 请求实现重试机制。
  • 自动化测试
    • 编写单元测试、集成测试和 E2E 测试,确保功能的正确性。
    • 进行性能测试,如 Lighthouse 审计、WebPageTest。
  • 灰度发布
    • 先向小部分用户发布新版本,收集反馈后再全量发布。

28、如何统计长任务时间、长任务执行次数

长任务是指执行时间超过 50ms 的任务,会导致页面卡顿。可以使用以下方法统计长任务:

  • 使用 PerformanceObserver API
    if (window.PerformanceObserver) {
      const observer = new PerformanceObserver((list) => {
        list.getEntries().forEach(entry => {
          console.log('长任务 detected:', entry.name, entry.duration);
          // 记录长任务信息到分析系统
          sendToAnalytics({
            name: entry.name,
            duration: entry.duration,
            startTime: entry.startTime
          });
        });
      });
    
      // 开始观察长任务
      observer.observe({ entryTypes: ['longtask'] });
    }
    
  • 自定义长任务检测
    function startTrackingLongTasks() {
      let lastTime = performance.now();
      let taskCount = 0;
    
      function checkTaskDuration() {
        const now = performance.now();
        const taskDuration = now - lastTime;
        lastTime = now;
    
        if (taskDuration > 50) {
          console.log(`长任务 detected: ${taskDuration}ms`);
          taskCount++;
        }
    
        requestAnimationFrame(checkTaskDuration);
      }
    
      requestAnimationFrame(checkTaskDuration);
    
      return () => {
        console.log(`Total long tasks: ${taskCount}`);
        return taskCount;
      };
    }
    
    // 使用示例
    const stopTracking = startTrackingLongTasks();
    // 一段时间后停止并获取统计结果
    // const count = stopTracking();
    

29、web 应用中如何对静态资源加载失败的场景做降级处理

针对静态资源加载失败的情况,可以采取以下降级策略:

  • 图片资源
    • 使用 onerror 事件:
      <img src="main-image.jpg" onerror="this.src='fallback-image.jpg'" alt="Main Image">
      
    • 封装图片组件,在组件内部处理加载失败逻辑。
  • CSS/JS 文件
    • 使用 onerror 事件动态加载备用资源:
      <script src="main.js" onerror="loadFallbackScript()"></script>
      <link href="main.css" rel="stylesheet" onerror="loadFallbackCSS()">
      
      <script>
        function loadFallbackScript() {
          const script = document.createElement('script');
          script.src = 'fallback.js';
          document.head.appendChild(script);
        }
      
        function loadFallbackCSS() {
          const link = document.createElement('link');
          link.href = 'fallback.css';
          link.rel = 'stylesheet';
          document.head.appendChild(link);
        }
      </script>
      
  • 字体资源
    • 使用 font-display 属性控制字体加载行为:
      @font-face {
        font-family: 'MyFont';
        src: url('myfont.woff2') format('woff2');
        font-display: swap; /* 字体加载时使用后备字体 */
      }
      
  • 监控与告警
    • 集成 Sentry 等监控工具,及时发现资源加载失败情况。
    • 记录资源加载失败日志,分析失败原因。

30、html 中前缀为 data-开头的元素属性是什么

data- 开头的属性是 HTML5 新增的自定义数据属性,用于在 DOM 元素上存储额外的、非呈现性的数据。

  • 基本用法
    <div id="user" data-id="123" data-name="John Doe" data-age="30">
      User Information
    </div>
    
  • JavaScript 访问
    const userElement = document.getElementById('user');
    // 访问 data-id
    const userId = userElement.dataset.id; // "123"
    // 访问 data-name
    const userName = userElement.dataset.name; // "John Doe"
    // 设置新的 data 属性
    userElement.dataset.role = 'admin';
    
  • 特点和用途
    • 属性名不能包含大写字母,会自动转换为小写。
    • 可以通过 CSS 选择器匹配:[data-active="true"]
    • 用途包括:为 JavaScript 存储数据、驱动样式变化、在模板中传递数据。
    • 不建议存储敏感信息,因为它们可以被客户端访问。

31、移动端如何实现上拉加载,下拉刷新

在移动端实现上拉加载和下拉刷新功能,可以采用以下方法:

  • 下拉刷新
    • 使用 touchstarttouchmovetouchend 事件监听用户手势。
    • 当用户下拉超过一定距离时,触发刷新操作。
    • 示例代码:
      let startY, scrollTop;
      const container = document.querySelector('.refresh-container');
      const refreshIndicator = document.querySelector('.refresh-indicator');
      
      container.addEventListener('touchstart', (e) => {
        if (container.scrollTop === 0) {
          startY = e.touches[0].clientY;
        }
      });
      
      container.addEventListener('touchmove', (e) => {
        if (startY) {
          const moveY = e.touches[0].clientY;
          const diff = moveY - startY;
          if (diff > 0) {
            // 阻止页面滚动
            e.preventDefault();
            // 显示刷新提示
            refreshIndicator.style.display = 'block';
            refreshIndicator.style.transform = `translateY(${diff / 2}px)`;
          }
        }
      });
      
      container.addEventListener('touchend', () => {
        if (startY) {
          // 执行刷新逻辑
          if (refreshIndicator.style.transform.includes('50px')) {
            refreshData();
          }
          // 重置状态
          refreshIndicator.style.display = 'none';
          refreshIndicator.style.transform = 'translateY(0)';
          startY = null;
        }
      });
      
  • 上拉加载
    • 监听滚动事件,当滚动到底部附近时触发加载更多。
    • 示例代码:
      const container = document.querySelector('.load-more-container');
      const loadingIndicator = document.querySelector('.loading-indicator');
      let isLoading = false;
      
      container.addEventListener('scroll', () => {
        if (isLoading) return;
      
        const { scrollTop, scrollHeight, clientHeight } = container;
        if (scrollTop + clientHeight >= scrollHeight - 50) {
          // 接近底部,加载更多
          isLoading = true;
          loadingIndicator.style.display = 'block';
          loadMoreData().then(() => {
            isLoading = false;
            loadingIndicator.style.display = 'none';
          });
        }
      });
      
  • 使用第三方库
    • better-scroll:一个专为移动端设计的滚动库,支持下拉刷新和上拉加载。
    • pulltorefresh.js:专注于实现下拉刷新功能的轻量级库。

32、如何判断 dom 元素是否在可视区域

判断 DOM 元素是否在可视区域,有以下几种方法:

  • 使用 getBoundingClientRect()
    function isElementInViewport(el) {
      const rect = el.getBoundingClientRect();
      return (
        rect.top >= 0 &&
        rect.left >= 0 &&
        rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
        rect.right <= (window.innerWidth || document.documentElement.clientWidth)
      );
    }
    
    // 使用示例
    const element = document.getElementById('target');
    if (isElementInViewport(element)) {
      console.log('元素在可视区域内');
    }
    
  • 使用 Intersection Observer API
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          console.log('元素进入可视区域', entry.target);
          // 可以在这里执行元素进入视口时的操作
        } else {
          console.log('元素离开可视区域', entry.target);
        }
      });
    });
    
    // 开始观察元素
    const target = document.getElementById('target');
    observer.observe(target);
    
  • 优缺点比较
    • getBoundingClientRect()
      • 优点:兼容性好,可精确控制检测逻辑。
      • 缺点:需要在滚动事件中频繁调用,可能影响性能。
    • Intersection Observer API
      • 优点:性能优化,异步回调,不会阻塞主线程。
      • 缺点:浏览器兼容性有限(IE 不支持),需要 polyfill。

33、前端如何用 canvas 来做电影院选票功能

使用 Canvas 实现电影院选票功能的核心思路如下:

  • 绘制座位图
    • 使用 Canvas API 绘制座位网格,不同状态的座位使用不同颜色表示。
    • 为每个座位计算位置和尺寸,存储座位状态数据。
  • 座位交互
    • 监听 Canvas 的点击事件,计算点击位置对应的座位。
    • 更新座位状态并重新绘制。
  • 示例代码
    <canvas id="cinema-seats" width="600" height="400"></canvas>
    <script>
      const canvas = document.getElementById('cinema-seats');
      const ctx = canvas.getContext('2d');
      const seatSize = 25;
      const seatSpacing = 5;
      const rows = 10;
      const cols = 15;
    
      // 座位数据:0-可选,1-已售,2-已选
      const seats = Array(rows).fill().map(() => Array(cols).fill(0));
    
      // 随机设置一些已售座位
      for (let i = 0; i < 30; i++) {
        const row = Math.floor(Math.random() * rows);
        const col = Math.floor(Math.random() * cols);
        seats[row][col] = 1;
      }
    
      // 绘制座位图
      function drawSeats() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
    
        // 绘制银幕
        ctx.fillStyle = '#333';
        ctx.roundRect(150, 30, 300, 50, 10);
        ctx.fill();
        ctx.fillStyle = '#fff';
        ctx.font = '20px Arial';
        ctx.textAlign = 'center';
        ctx.fillText('银幕', 300, 60);
    
        // 绘制座位
        for (let row = 0; row < rows; row++) {
          for (let col = 0; col < cols; col++) {
            const x = 50 + col * (seatSize + seatSpacing);
            const y = 120 + row * (seatSize + seatSpacing);
    
            // 根据座位状态设置颜色
            switch (seats[row][col]) {
              case 0: // 可选
                ctx.fillStyle = '#ccc';
                break;
              case 1: // 已售
                ctx.fillStyle = '#ff6b6b';
                break;
              case 2: // 已选
                ctx.fillStyle = '#48dbfb';
                break;
            }
    
            // 绘制座位
            ctx.roundRect(x, y, seatSize, seatSize, 5);
            ctx.fill();
    
            // 绘制座位编号
            ctx.fillStyle = '#333';
            ctx.font = '12px Arial';
            ctx.textAlign = 'center';
            ctx.fillText(`${String.fromCharCode(65 + row)}${col + 1}`, x + seatSize / 2, y + seatSize / 2 + 4);
          }
        }
      }
    
      // 处理座位点击
      canvas.addEventListener('click', (e) => {
        const rect = canvas.getBoundingClientRect();
        const clickX = e.clientX - rect.left;
        const clickY = e.clientY - rect.top;
    
        // 计算点击的是哪个座位
        const col = Math.floor((clickX - 50) / (seatSize + seatSpacing));
        const row = Math.floor((clickY - 120) / (seatSize + seatSpacing));
    
        // 检查坐标是否在有效范围内
        if (row >= 0 && row < rows && col >= 0 && col < cols) {
          // 只有可选座位可以被选中
          if (seats[row][col] === 0) {
            seats[row][col] = 2;
            drawSeats();
            console.log(`选中座位: ${String.fromCharCode(65 + row)}${col + 1}`);
          } else if (seats[row][col] === 2) {
            // 取消选中
            seats[row][col] = 0;
            drawSeats();
          }
        }
      });
    
      // 初始绘制
      drawSeats();
    </script>
    

34、如何通过设置失效时间清除本地存储的数据

要给 localStorage 中的数据设置失效时间,可以封装一个工具函数,在存储数据时同时记录时间戳,读取时检查是否过期。

class ExpirableStorage {
  constructor(storage) {
    this.storage = storage;
  }

  // 设置带过期时间的数据
  setItem(key, value, expiresInSeconds) {
    const item = {
      value,
      expiresAt: Date.now() + (expiresInSeconds * 1000)
    };
    this.storage.setItem(key, JSON.stringify(item));
  }

  // 获取数据,自动处理过期
  getItem(key) {
    const itemStr = this.storage.getItem(key);
    if (!itemStr) return null;

    const item = JSON.parse(itemStr);
    if (Date.now() > item.expiresAt) {
      this.storage.removeItem(key);
      return null;
    }

    return item.value;
  }

  // 移除数据
  removeItem(key) {
    this.storage.removeItem(key);
  }

  // 清除所有数据
  clear() {
    this.storage.clear();
  }
}

// 使用示例
const expirableLocalStorage = new ExpirableStorage(localStorage);

// 存储一个 10 秒后过期的数据
expirableLocalStorage.setItem('token', 'abc123', 10);

// 获取数据
const token = expirableLocalStorage.getItem('token');
if (token) {
  console.log('Token:', token);
} else {
  console.log('Token 已过期或不存在');
}

35、如果不使用脚手架,如何用 webpack 构建一个自己的 react 应用

不使用脚手架,手动配置 Webpack 构建 React 应用的步骤如下:

  1. 初始化项目
    mkdir my-react-app && cd my-react-app
    npm init -y
    
  2. 安装依赖
    npm install react react-dom
    npm install --save-dev webpack webpack-cli webpack-dev-server babel-loader @babel/core @babel/preset-env @babel/preset-react html-webpack-plugin
    
  3. 创建项目结构
    my-react-app/
    ├── src/
    │   ├── index.js
    │   └── App.js
    ├── public/
    │   └── index.html
    ├── webpack.config.js
    └── package.json
    
  4. 配置 Babel
    创建 .babelrc 文件:
    {
      "presets": ["@babel/preset-env", "@babel/preset-react"]
    }
    
  5. 配置 Webpack
    webpack.config.js 文件内容:
    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    
    module.exports = {
      entry: './src/index.js',
      output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js',
        clean: true
      },
      devServer: {
        static: path.resolve(__dirname, 'dist'),
        port: 3000,
        open: true,
        hot: true
      },
      module: {
        rules: [
          {
            test: /\.(js|jsx)$/,
            exclude: /node_modules/,
            use: {
              loader: 'babel-loader'
            }
          },
          {
            test: /\.css$/,
            use: ['style-loader', 'css-loader']
          }
        ]
      },
      plugins: [
        new HtmlWebpackPlugin({
          template: './public/index.html'
        })
      ],
      resolve: {
        extensions: ['.js', '.jsx']
      }
    };
    
  6. 创建 HTML 模板
    public/index.html 文件内容:
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>React App</title>
    </head>
    <body>
      <div id="root"></div>
    </body>
    </html>
    
  7. 创建 React 组件
    src/App.js 文件内容:
    import React from 'react';
    
    function App() {
      return (
        <div className="App">
          <h1>Hello, React!</h1>
        </div>
      );
    }
    
    export default App;
    
    src/index.js 文件内容:
    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import App from './App';
    
    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(<App />);
    
  8. 添加 npm 脚本
    package.json 中添加:
    {
      "scripts": {
        "start": "webpack-dev-server --mode development",
        "build": "webpack --mode production"
      }
    }
    
  9. 运行项目
    npm start # 开发环境
    npm run build # 生产环境构建
    

36、用 Node.js 实现一个命令行工具,统计输入目录下面指定文件代码的行数

下面是一个统计指定目录下指定类型文件代码行数的 Node.js 命令行工具实现:

#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { program } = require('commander');

// 配置命令行参数
program
  .version('1.0.0')
  .option('-d, --directory <dir>', '目录路径', process.cwd())
  .option('-e, --extension <ext>', '文件扩展名,多个用逗号分隔', 'js,jsx,ts,tsx')
  .option('-i, --ignore <dirs>', '忽略的目录,多个用逗号分隔', 'node_modules,dist,.git')
  .parse(process.argv);

const options = program.opts();
const targetDir = path.resolve(options.directory);
const extensions = options.extension.split(',').map(ext => ext.trim());
const ignoreDirs = options.ignore.split(',').map(dir => dir.trim());

// 统计结果
const stats = {
  files: 0,
  totalLines: 0,
  blankLines: 0,
  codeLines: 0,
  commentLines: 0
};

// 检查文件是否是目标扩展名
function isTargetFile(filePath) {
  const ext = path.extname(filePath).substring(1);
  return extensions.includes(ext);
}

// 检查目录是否应该被忽略
function shouldIgnore(dirPath) {
  const dirName = path.basename(dirPath);
  return ignoreDirs.includes(dirName);
}

// 递归统计目录中的文件
function countLinesInDirectory(dirPath) {
  try {
    const entries = fs.readdirSync(dirPath, { withFileTypes: true });
    
    for (const entry of entries) {
      const entryPath = path.join(dirPath, entry.name);
      
      if (entry.isDirectory()) {
        if (!shouldIgnore(entryPath)) {
          countLinesInDirectory(entryPath);
        }
      } else if (entry.isFile() && isTargetFile(entryPath)) {
        countFileLines(entryPath);
      }
    }
  } catch (err) {
    console.error(`Error reading directory ${dirPath}:`, err.message);
  }
}

// 统计单个文件的行数
function countFileLines(filePath) {
  try {
    const content = fs.readFileSync(filePath, 'utf8');
    const lines = content.split('\n');
    
    stats.files++;
    stats.totalLines += lines.length;
    
    let commentLines = 0;
    let inMultiLineComment = false;
    
    for (const line of lines) {
      const trimmedLine = line.trim();
      
      // 空行
      if (trimmedLine === '') {
        stats.blankLines++;
        continue;
      }
      
      // 多行注释检查
      if (inMultiLineComment) {
        commentLines++;
        if (trimmedLine.includes('*/')) {
          inMultiLineComment = false;
        }
        continue;
      }
      
      // 单行注释
      if (trimmedLine.startsWith('//')) {
        commentLines++;
        continue;
      }
      
      // 多行注释开始
      if (trimmedLine.startsWith('/*')) {
        commentLines++;
        if (!trimmedLine.includes('*/')) {
          inMultiLineComment = true;
        }
        continue;
      }
      
      // 代码行
      stats.codeLines++;
    }
    
    stats.commentLines += commentLines;
  } catch (err) {
    console.error(`Error reading file ${filePath}:`, err.message);
  }
}

// 执行统计
countLinesInDirectory(targetDir);

// 输出结果
console.log(`\n统计结果 (目录: ${targetDir}):`);
console.log('---------------------------------');
console.log(`文件数量: ${stats.files}`);
console.log(`总行数: ${stats.totalLines}`);
console.log(`空行数: ${stats.blankLines}`);
console.log(`代码行数: ${stats.codeLines}`);
console.log(`注释行数: ${stats.commentLines}`);
console.log('---------------------------------');
console.log(`代码行占比: ${((stats.codeLines / stats.totalLines) * 100).toFixed(2)}%`);
console.log(`注释行占比: ${((stats.commentLines / stats.totalLines) * 100).toFixed(2)}%`);

使用方法:

  1. 将上面的代码保存为 line-counter.js
  2. 安装依赖:npm install commander
  3. 运行命令:
    node line-counter.js -d ./src -e js,jsx,ts -i node_modules,dist
    
    这将统计 ./src 目录下所有 .js.jsx.ts 文件的代码行数,忽略 node_modulesdist 目录。

37、package.json 里面 sideEffects 属性的作用是什么

package.json 中的 sideEffects 属性用于告诉打包工具(如 Webpack、Rollup)哪些文件是有副作用的,不能进行 Tree-shaking(摇树优化)。

  • 基本作用
    • 标明哪些文件或模块在导入时会执行额外的操作,不能简单地被移除。
    • 帮助打包工具更准确地进行死代码消除,提高打包效率。
  • 使用示例
    {
      "name": "my-library",
      "sideEffects": false, // 所有文件都没有副作用
      // 或者指定有副作用的文件
      "sideEffects": [
        "*.css", // CSS 文件有副作用
        "src/side-effect.js" // 特定文件有副作用
      ]
    }
    
  • Tree-shaking 原理
    • 对于没有副作用的模块,如果代码没有被使用,打包时可以安全地移除。
    • 对于有副作用的模块,即使代码没有被直接使用,也需要保留。
  • 注意事项
    • 只有使用 ES6 模块语法(import/export)才能进行 Tree-shaking。
    • 对于 CSS 文件、polyfills、全局变量初始化等通常有副作用,需要特别指定。

38、script 标签上有哪些属性,分别作用是什么

<script> 标签的常用属性及其作用如下:

  • src:指定外部脚本文件的 URL。
    <script src="script.js"></script>
    
  • type:指定脚本的类型,默认是 text/javascript
    • module:将脚本作为 ES6 模块处理。
    <script type="module" src="module.js"></script>
    
  • async:异步加载脚本,不阻塞页面渲染,脚本加载完成后立即执行。
    <script async src="async-script.js"></script>
    
  • defer:延迟加载脚本,页面解析完成后按顺序执行。
    <script defer src="defer-script.js"></script>
    
  • charset:指定外部脚本的字符编码,很少使用。
    <script src="script.js" charset="UTF-8"></script>
    
  • crossorigin:配置跨域资源共享(CORS)。
    • anonymous:不发送凭证信息。
    • use-credentials:发送凭证信息。
    <script crossorigin="anonymous" src="https://example.com/script.js"></script>
    
  • integrity:用于验证脚本的完整性,防止内容被篡改。
    <script
      src="https://cdn.example.com/script.js"
      integrity="sha384-..."
      crossorigin="anonymous"
    ></script>
    
  • nomodule:仅在浏览器不支持 ES6 模块时执行。
    <script nomodule src="fallback-script.js"></script>
    

39、为什么 SPA 应用都会提供一个 hash 路由,好处是什么

SPA(单页应用)使用 hash 路由(URL 中的 # 部分)主要有以下好处:

  • 无需服务器支持:hash 值的变化不会向服务器发送请求,所有路由逻辑都在客户端处理。
  • 历史记录管理:浏览器会记录 hash 值的变化,支持前进后退操作。
  • 兼容性好:所有现代浏览器都支持 hashchange 事件。
  • 简单实现:相比 HTML5 History API,hash 路由实现更简单。
  • 避免 404 错误:刷新页面时,服务器只需要返回同一个 HTML 文件,由客户端处理路由。
  • 适合简单应用:对于小型 SPA,hash 路由足以满足需求。
  • 示例
    https://example.com/#/home  → 显示主页
    https://example.com/#/about → 显示关于页
    

40、如何进行路由变化监听

在 React 中监听路由变化有多种方式,具体取决于使用的路由库:

  • 使用 react-router-dom(v6)
    import { useEffect } from 'react';
    import { useLocation } from 'react-router-dom';
    
    function App() {
      const location = useLocation();
    
      useEffect(() => {
        console.log('路由变化:', location.pathname);
        // 这里可以执行路由变化后的操作
        // 例如:页面滚动到顶部、发送页面访问统计等
      }, [location]);
    
      return (
        <div>
          {/* 应用内容 */}
        </div>
      );
    }
    
  • 使用 withRouter(react-router-dom v5 及以下)
    import { withRouter } from 'react-router-dom';
    
    class MyComponent extends React.Component {
      componentDidUpdate(prevProps) {
        if (prevProps.location.pathname !== this.props.location.pathname) {
          console.log('路由变化:', this.props.location.pathname);
        }
      }
    
      render() {
        return <div>组件内容</div>;
      }
    }
    
    export default withRouter(MyComponent);
    
  • 手动监听 History 对象
    import { useEffect } from 'react';
    import { useHistory } from 'react-router-dom';
    
    function MyComponent() {
      const history = useHistory();
    
      useEffect(() => {
        const unlisten = history.listen((location, action) => {
          console.log('路由变化:', location.pathname, action);
        });
    
        return () => {
          unlisten(); // 组件卸载时取消监听
        };
      }, [history]);
    
      return <div>组件内容</div>;
    }
    

41、单点登录是什么,具体流程是什么

单点登录(Single Sign-On,SSO)是一种身份验证机制,允许用户使用一组凭证(如用户名和密码)访问多个相关系统或应用。

  • 基本流程
    1. 用户访问应用 A:用户尝试访问应用 A,应用 A 检测到用户未登录,将用户重定向到认证中心。
    2. 认证中心登录:用户在认证中心输入凭证进行登录。
    3. 颁发令牌:认证中心验证凭证后,创建并颁发一个身份令牌(如 JWT、Session ID)。
    4. 重定向回应用 A:认证中心将用户重定向回应用 A,并附带身份令牌。
    5. 验证令牌:应用 A 验证令牌的有效性,确认用户身份后为用户提供访问权限。
    6. 访问应用 B:用户尝试访问应用 B,应用 B 检测到用户未登录,将用户重定向到认证中心。
    7. 检查会话:认证中心发现用户已登录,直接颁发令牌给应用 B。
    8. 访问授权:应用 B 验证令牌后为用户提供访问权限。
  • 常见实现方式
    • 基于 Cookie:认证中心和应用共享同一个域名下的 Cookie。
    • 令牌机制:如 JWT(JSON Web Token),包含用户身份信息和签名。
    • OAuth 2.0/OpenID Connect:标准的授权和身份验证协议。
  • 优点
    • 提升用户体验,减少重复登录。
    • 简化管理,集中身份验证。
    • 增强安全性,统一的身份管理。
  • 缺点
    • 单点故障风险,认证中心故障会影响所有应用。
    • 复杂度较高,需要处理跨域、令牌安全等问题。

42、web 网页如何禁止别人移除水印

在网页中实现水印防移除可以采用以下综合方案:

  • 使用 Canvas 绘制水印
    function createWatermark() {
      const canvas = document.createElement('canvas');
      canvas.width = 200;
      canvas.height = 100;
      
      const ctx = canvas.getContext('2d');
      ctx.rotate(-20 * Math.PI / 180);
      ctx.font = '16px Arial';
      ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
      ctx.fillText('Confidential', 50, 50);
      
      return canvas.toDataURL('image/png');
    }
    
    function applyWatermark() {
      const watermarkUrl = createWatermark();
      document.body.style.backgroundImage = `url("${watermarkUrl}")`;
      document.body.style.backgroundRepeat = 'repeat';
    }
    
    applyWatermark();
    
  • 监听 DOM 变化
    function protectWatermark() {
      const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
          // 检查是否有人尝试修改或移除水印相关元素
          if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
            if (mutation.target === document.body) {
              // 恢复水印样式
              applyWatermark();
            }
          }
        });
      });
    
      // 开始观察 body 元素的变化
      observer.observe(document.body, {
        attributes: true,
        childList: true,
        subtree: true
      });
    }
    
    protectWatermark();
    
  • 阻止右键菜单和选择
    // 阻止右键菜单
    document.addEventListener('contextmenu', e => {
      e.preventDefault();
    });
    
    // 阻止选择文本
    document.addEventListener('selectstart', e => {
      e.preventDefault();
    });
    
  • 使用多层水印
    在不同的 DOM 层级添加多个水印元素,增加移除难度。
  • 定期检查和恢复
    setInterval(() => {
      if (!document.body.style.backgroundImage.includes('data:image/png')) {
        applyWatermark();
      }
    }, 1000);
    
  • 注意事项
    • 这些方法只能增加移除水印的难度,无法完全阻止技术高超的用户。
    • 过度保护可能影响用户体验,需权衡利弊。
    • 对于重要内容,建议结合服务器端验证和版权保护措施。

四、项目踩坑回答范例

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

五、看到一篇特别好的前端八股文,强力推荐

🔥 连八股文都不懂还指望在前端混下去么

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值