1. 请简述你对 react 的理解
React 是 Facebook 推出的用于构建用户界面的声明式、组件化 JavaScript 库,核心思想是“组件化”与“数据驱动视图”,不直接操作真实 DOM,而是通过虚拟 DOM 优化渲染性能。
核心特点:① 声明式编程:只需描述 UI 的目标状态,React 自动处理 DOM 更新逻辑,无需关注过程;② 组件化:将 UI 拆分为独立可复用的组件,组件内部维护自身状态与逻辑,便于开发和维护;③ 单向数据流:数据从父组件通过 props 向下传递,子组件无法直接修改 props,状态变更需通过回调函数向上反馈,避免数据混乱;④ 虚拟 DOM 与 Diff 算法:用 JS 对象模拟真实 DOM,通过 Diff 算法对比新旧虚拟 DOM 差异,仅更新变化部分到真实 DOM,减少性能开销;⑤ 跨平台适配:可通过 React Native 开发原生应用,通过 Next.js 实现服务端渲染(SSR),提升首屏加载速度和 SEO 效果。
2. state 和 props 的区别
| 对比维度 | state(状态) | props(属性) |
|---|---|---|
| 数据来源 | 组件内部定义,由组件自身维护 | 父组件传递,组件自身无法修改 |
| 可变性 | 可变,通过 setState(类组件)或 useState(函数组件)修改 | 不可变,组件仅能读取,修改需由父组件更新传递 |
| 作用范围 | 仅作用于当前组件内部 | 可在父组件传递给子组件,实现跨组件数据传递 |
| 初始化方式 | 类组件:constructor 中初始化;函数组件:useState Hook 初始化 | 组件调用时通过属性传入,可设置默认值(defaultProps) |
3. 讲下组件之间的数据传递
React 组件间数据传递需根据组件关系选择对应方式,核心场景及方案如下:
- 父传子:通过 props 传递。父组件调用子组件时,将数据/方法作为属性传入,子组件通过 props 接收使用,适用于直接父子关系,简单高效。
- 子传父:通过回调函数。父组件传递一个回调函数给子组件,子组件触发该函数时将数据作为参数传入,父组件在回调中接收数据。
- 跨层级传递(爷孙/远亲):① Context API:创建全局上下文,上层组件通过 Provider 提供数据,下层组件通过 Consumer 或 useContext 获取数据,无需层层透传;② 状态管理库(Redux/MobX/Recoil):适用于大型项目,集中管理全局状态,任意组件可通过 API 获取/修改状态。
- 兄弟组件传递:借助共同父组件中转。A 组件将数据传递给父组件,父组件再通过 props 传递给 B 组件;或直接使用 Context/状态管理库。
4. vue 与 react 的区别
| 对比维度 | Vue | React |
|---|---|---|
| 核心思想 | 渐进式框架,兼顾声明式和命令式,更注重易用性 | 声明式编程,组件化,更注重函数式思想 |
| 模板语法 | 支持 HTML 模板(主流)+ JSX,模板贴近 HTML,学习成本低 | 推荐使用 JSX,将 HTML 融入 JS,灵活性高,适合复杂 UI 逻辑 |
| 状态管理 | Vue2 用 Vuex,Vue3 用 Pinia(简化版 Vuex),内置响应式系统 | 无内置状态管理,需使用 Redux/MobX/Recoil 等第三方库 |
| 响应式原理 | Vue2:Object.defineProperty 监听属性 get/set;Vue3:Proxy 代理整个对象 | 通过 setState/useState 触发重新渲染,对比虚拟 DOM 差异更新 |
| 组件通信 | props/emit、Provide/Inject、Vuex/Pinia、EventBus | props/回调、Context API、状态管理库、HOC |
| 适用场景 | 中小型项目、快速开发、对易用性要求高的场景 | 大型项目、复杂 UI 逻辑、跨平台开发(React Native) |
5. 请简述虚拟 dom 和 diff 算法
(1)虚拟 DOM(Virtual DOM)
虚拟 DOM 是用 JavaScript 对象模拟真实 DOM 树的结构,包含节点类型、属性、子节点等信息(如{ tag: 'div', props: { id: 'app' }, children: [] })。它是对真实 DOM 的抽象描述,不依赖浏览器环境,可在内存中高效操作。
核心作用:① 避免直接操作真实 DOM:真实 DOM 操作开销大,虚拟 DOM 通过内存对象操作替代真实 DOM 操作,减少性能损耗;② 跨平台兼容:虚拟 DOM 可被渲染为不同平台的视图(如浏览器 DOM、React Native 原生组件)。
(2)Diff 算法
Diff 算法是虚拟 DOM 的核心配套算法,用于对比新旧两棵虚拟 DOM 树的差异,计算出“最小更新集”,最终只将差异部分更新到真实 DOM,避免全量渲染。
核心设计思路:① 同层比较:只对比同一层级的节点,不跨层续多次调用 setState 会被合并,推荐使用函数形式(prev => ({ count: prev.count + 1 }))避免合并问题。
7. react 生命周期函数
React 生命周期分为三个核心阶段:挂载(Mounting)、更新(Updating)、卸载(Unmounting),React 16.3+ 后推荐使用函数组件 +Hook,类组件生命周期如下:
- 挂载阶段(组件创建并插入 DOM):constructor(初始化 state、绑定 this)→ getDerivedStateFromProps(从 props 派生 state)→ render(生成虚拟 DOM)→ componentDidMount(组件挂载完成,可执行 DOM 操作、请求数据)。
- 更新阶段(state/props 变化触发):getDerivedStateFromProps → shouldComponentUpdate(判断是否需要重新渲染)→ render → getSnapshotBeforeUpdate(更新 DOM 前获取快照)→ componentDidUpdate(DOM 更新完成,可做后续操作)。
- 卸载阶段(组件从 DOM 移除):componentWillUnmount(组件卸载前执行,清理副作用:清除定时器、解绑事件、取消请求等)。
废弃钩子:componentWillMount、componentWillReceiveProps、componentWillUpdate,推荐用其他钩子替代。
8. 为什么虚拟 dom 会提高性能
虚拟 DOM 提高性能的核心逻辑是“减少真实 DOM 的操作次数和范围”,具体原因如下:
- 真实 DOM 操作开销大:真实 DOM 关联着浏览器的布局、样式计算等复杂逻辑,频繁操作会触发多次重排(Reflow)和重绘(Repaint),性能损耗严重;而虚拟 DOM 是内存中的 JavaScript 对象,操作成本极低。
- 批量更新与最小差异更新:虚拟 DOM 通过 Diff 算法对比新旧树差异,只将变化的部分同步到真实 DOM,而非全量替换。例如,列表中仅修改一个元素的文本,虚拟 DOM 只会更新该元素的文本节点。
- 避免不必要的 DOM 操作:虚拟 DOM 在内存中整合多次数据变化,批量触发一次真实 DOM 更新。例如,连续修改两次 state,虚拟 DOM 会合并两次变化,只执行一次真实 DOM 更新。
9. shouldComponentUpdate 是做什么的?
shouldComponentUpdate 是 React 类组件的生命周期钩子函数,用于“判断组件是否需要重新渲染”,返回布尔值(默认返回 true)。
核心作用:优化性能,避免不必要的重新渲染。当组件的 props 或 state 变化时,React 会先调用 shouldComponentUpdate,若返回 false,会跳过后续的 render、Diff 算法和真实 DOM 更新流程,直接终止本次渲染。
使用场景:当组件的 props/state 变化但不会影响 UI 时,手动返回 false 阻止渲染。纯组件(PureComponent)内置了该钩子的浅比较逻辑,会自动对比 props 和 state 的浅值。
10. react diff 原理
React Diff 算法是对比新旧虚拟 DOM 差异的核心算法,核心目标是高效找出最小更新集,其核心原理可概括为“同层比较、类型判断、key 优化”三大要点:
- 同层比较(层级遍历):只对比同一层级的节点,不跨层级比较,降低算法复杂度(时间复杂度从 O(n³)优化为 O(n))。若父节点类型变化,直接销毁旧父节点及其所有子节点,无需深入子节点对比。
- 节点类型判断:若节点类型不同(如 div→p),直接销毁旧节点并创建新节点;若类型相同,对比属性差异并更新,再递归对比子节点。
- 列表优化:通过 key 标识列表元素唯一性,快速判断列表元素的新增、删除、移动,避免无 key 时按索引匹配导致的不必要渲染和状态错乱。
11. 什么是受控组件
受控组件是 React 中表单元素的一种处理方式,其值由组件的 state 控制,表单元素的状态与 React 状态“双向绑定”。
核心特点:① 表单元素(input、textarea、select 等)的值由 state 驱动;② 通过 onChange 事件监听用户输入,触发 setState 更新 state,进而更新表单元素的值;③ 组件状态完全受 React 控制,可方便地对输入进行验证、格式化等处理。
示例:<input type="text" value={this.state.value} onChange={(e) => this.setState({ value: e.target.value })} />
非受控组件:值由 DOM 自身维护,通过 ref 获取 DOM 元素的值,适用于简单场景(如文件上传 input[type="file"])。
12. 组件的状态(state)和属性(props)之间有什么不同?
(与第 2 题一致,核心差异如下)
- 数据来源不同:state 是组件内部定义的私有状态,由组件自身维护;props 是父组件传递给子组件的数据,组件自身无法修改。
- 可变性不同:state 是可变的,通过 setState/useState 修改;props 是不可变的,子组件仅能读取,修改需由父组件更新传递。
- 作用范围不同:state 仅作用于当前组件内部;props 可在父组件传递给子组件,实现跨组件数据传递。
- 初始化方式不同:state 在组件内部初始化(constructor/useState);props 在组件调用时由父组件传入,可设置默认值。
13. 调用 super(props)的目的是什么?
super(props)用于 React 类组件的 constructor 中,核心目的有两个:
- 调用父类(React.Component)的构造函数,完成父类的初始化工作,确保组件继承的属性和方法能正常使用。
- 将 props 传递给父类构造函数,使得在 constructor 中可以通过 this.props 访问到 props(若不传递 props,constructor 中 this.props 为 undefined,但 render 等其他生命周期中仍可访问)。
注意:在 React 类组件中,若定义了 constructor,必须在其中调用 super(),且 super()必须是 constructor 中的第一个语句;若需要在 constructor 中使用 props,必须传递 super(props)。
14. react 中构建组件的方式
React 中构建组件主要有三种方式,各有适用场景:
- 函数组件(推荐,React 16.8+):用普通函数定义的组件,接收 props 作为参数,返回 JSX。简洁轻便,无 this 问题,通过 Hook(useState、useEffect 等)实现状态管理和生命周期功能。适用于大多数场景。示例:const MyComponent = (props) => 。
- 类组件:继承 React.Component 的类,通过 render 方法返回 JSX。可维护自身 state,使用生命周期钩子。适用于复杂状态管理和生命周期逻辑的场景(React 16.8 后可被函数组件 +Hook 替代)。示例:class MyComponent extends React.Component { render() { return } }。
- 纯组件(PureComponent):继承 React.PureComponent 的类组件,内置了 shouldComponentUpdate 的浅比较逻辑,当 props 和 state 浅比较无变化时,不触发重新渲染。适用于 props 和 state 为简单类型、无深层嵌套的组件,可优化性能。
15. 什么是高阶组件 HOC
高阶组件(Higher-Order Component,HOC)是 React 中复用组件逻辑的高级技巧,本质是一个“函数”,接收一个或多个组件作为参数,返回一个新的增强组件。
核心作用:抽离组件的公共逻辑,实现逻辑复用(如权限控制、数据请求、日志打印等),不修改原组件,而是通过包装增强原组件的功能。
实现方式:① 属性代理(最常用):通过包裹原组件,向原组件传递增强的 props;② 反向继承:继承原组件,重写原组件的 render 或生命周期方法。
示例(属性代理):const withAuth = (WrappedComponent) => { return (props) => { const isLogin = localStorage.getItem('token'); return isLogin ? <WrappedComponent {...props} /> : ; } }; 使用:const AuthComponent = withAuth(MyComponent);
16. 什么是 hook
Hook 是 React 16.8 推出的新特性,允许在函数组件中使用状态(state)、生命周期、上下文(Context)等 React 特性,无需编写类组件。
核心作用:解决类组件的痛点(如 this 指向混乱、生命周期逻辑分散、代码复用复杂),让函数组件能实现类组件的所有功能,同时使代码更简洁、逻辑更聚合。
常用 Hook:① useState:用于声明状态变量,替代类组件的 this.state;② useEffect:处理副作用(如数据请求、DOM 操作),替代 componentDidMount 等生命周期;③ useContext:获取 Context 中的数据,简化跨层级数据传递;④ useReducer:用于复杂状态管理;⑤ useRef:获取 DOM 元素或保存持久化的值。
使用规则:① 只能在函数组件或自定义 Hook 的顶层调用;② 不能在循环、条件判断或嵌套函数中调用;③ 只能在 React 函数中调用。
17. 无状态组件的特点
无状态组件(Stateless Component)早期指“不依赖 state,仅接收 props 并返回 JSX”的函数组件,React 16.8 后 Hook 推出,现在更强调“不维护自身状态,仅作为 UI 展示”的组件。
核心特点:① 无自身状态(state),数据完全依赖 props 传入;② 是纯函数:相同的 props 输入,必然返回相同的 JSX 输出,无副作用;③ 代码简洁:无需编写类、constructor 等冗余代码;④ 性能更优:React 对其渲染优化更好,无需处理类组件的生命周期和状态管理逻辑;⑤ 易于测试:纯函数特性使其测试更简单。
适用场景:纯 UI 展示组件(如按钮、卡片、列表项)。
18. 三种请求方式的区别(ajax,axios,fetch)
| 对比维度 | AJAX(原生 XHR) | Axios | Fetch |
|---|---|---|---|
| 本质 | 原生 XHR 对象,异步请求基础 | 基于 XHR 封装的第三方库(Promise 风格) | ES6 原生 API(Promise 风格),替代 XHR |
| 语法复杂度 | 繁琐,需手动处理状态、事件 | 简洁,支持链式调用和 async/await | 简洁,原生支持 Promise |
| 功能特性 | 基础 GET/POST 请求,无扩展功能 | 内置拦截器、自动 JSON 转换、取消请求、超时设置 | 支持 CORS、Stream 流,无内置 JSON 转换等功能 |
| 错误处理 | 需手动判断 status 和 readyState | 统一通过.catch()捕获所有错误 | 仅捕获网络错误,4xx/5xx 视为成功 |
| 兼容性 | 兼容 IE6+ | 依赖 Promise,IE 需 polyfill | 现代浏览器支持,IE 不支持 |
19. 什么是 react 状态提升
状态提升是 React 中解决“多个兄弟组件共享状态”的核心模式,指将多个组件需要共享的状态“提升”到它们的共同父组件中,由父组件统一管理该状态,再通过 props 将状态和修改状态的方法传递给子组件。
核心逻辑:兄弟组件之间无法直接通信,通过共同父组件作为“中介”,实现状态共享和同步。
适用场景:多个组件需要基于同一状态进行 UI 渲染,或一个组件的状态变化需要同步到其他组件(如两个输入框联动)。
20. 什么是 webpack
Webpack 是一款前端模块化打包工具,核心作用是“将前端项目中的多个模块化文件(JS、CSS、图片、字体等)打包为浏览器可识别的静态资源”,同时提供代码转换、优化、分割、热更新等功能。
核心特点:① 模块化支持:支持 CommonJS、ES Module 等多种模块化规范;② 资源处理:通过 Loader 处理非 JS 资源;③ 插件系统:通过 Plugin 实现扩展功能(如代码压缩、自动生成 HTML);④ 开发优化:提供开发服务器、热模块替换、Source Map 等功能。
21. webpack 的组成
Webpack 的核心组成部分包括 4 个核心模块:
- 入口(Entry):指定 Webpack 的打包入口文件,即从哪个文件开始分析依赖关系,默认入口为./src/index.js。
- 出口(Output):指定打包输出目录和输出文件命名规则,默认输出目录为./dist,默认输出文件为 main.js。
- 加载器(Loader):处理非 JS 资源(如 CSS、图片),将其转换为 Webpack 可识别的模块,常用 Loader 有 css-loader、babel-loader、file-loader 等。
- 插件(Plugin):扩展 Webpack 功能,解决 Loader 无法处理的问题,常用 Plugin 有 HtmlWebpackPlugin、TerserPlugin 等。
22. webpack 打包原理
Webpack 打包的核心原理是“模块化分析 → 资源转换 → 依赖整合 → 输出静态资源”,具体流程:
- 初始化与配置解析:读取 webpack.config.js 配置,初始化打包配置,创建 Compiler 对象。
- 入口文件分析与依赖收集:从 Entry 入口文件开始,解析文件内容,收集依赖关系,构建依赖树。
- Loader 转换非 JS 资源:通过 Loader 将非 JS 资源(如 CSS、图片)转换为 JS 模块。
- 模块整合与代码生成:将所有转换后的模块整合为一个或多个 chunk,生成最终的静态资源文件,注入模块化运行时代码。
- Plugin 干预打包流程:在打包各阶段执行扩展功能(如压缩、生成 HTML)。
23. webpack 的工作过程
Webpack 的工作过程按以下 6 个阶段执行:
- 启动阶段:通过命令行或 API 启动 Webpack,读取并合并配置,初始化 Compiler 对象,注册 Plugin。
- 编译阶段:解析模块路径,通过 Loader 转换模块,解析模块内容生成 AST,收集依赖关系,构建依赖树。
- 模块优化阶段:进行代码分割、Tree-shaking(剔除死代码)等优化。
- chunk 优化阶段:确定 chunk 输出文件名,注入 Runtime 代码,生成最终 chunk 资产。
- 输出阶段:将 chunk 资产写入本地文件系统。
- 完成阶段:输出打包结果,若出错则终止流程并提示错误。
24. 什么是 typescript?
TypeScript(简称 TS)是微软开发的开源编程语言,是 JavaScript 的超集,核心扩展是“静态类型系统”。
核心特性:① 静态类型检查:编译时检查变量类型,提前发现错误;② 类型定义:支持基本类型、复杂类型(接口、泛型等);③ 兼容 JavaScript:所有 JS 代码可直接在 TS 中运行,编译后生成纯 JS;④ 增强 IDE 支持:提供代码提示、自动补全、重构等功能。
核心作用:提升代码质量,优化开发体验,适配大型项目协作。
25. react 中 keys 的作用是什么?
keys 是 React 中用于标识列表元素唯一性的特殊属性,核心作用是帮助 React 的 Diff 算法高效识别列表中的元素,优化渲染性能。
具体作用:① 唯一性标识:帮助 React 判断列表元素是新增、删除还是移动;② 提高 Diff 效率:通过 key 匹配元素,仅更新变化的元素,减少 DOM 操作;③ 保持元素状态:确保列表中有状态组件(如输入框)的状态与数据正确关联,避免排序后状态错位。
使用规则:优先使用后端返回的唯一标识(如 id)作为 key,避免使用索引。
26. react 事件处理中如何修改 this 的指向
React 类组件中,事件处理函数的 this 默认是 undefined,需手动绑定,常用方法有 4 种:
- 构造函数中绑定(推荐):在 constructor 中通过 this.handleClick = this.handleClick.bind(this)绑定。
- 事件绑定处使用箭头函数:<button onClick={(e) => this.handleClick(e)}> 点击 (可能影响性能)。
- 事件处理函数定义为箭头函数:handleClick = () => { console.log(this); }(简洁,无需手动绑定)。
- 事件绑定处使用 bind 绑定: 点击 (可能影响性能)。
函数组件中无 this 问题,直接使用 props 或 state 即可。
27. react 如何实现组件传值?
(与第 3 题一致,核心方案如下)
- 父传子:通过 props 传递数据/方法。
- 子传父:通过回调函数传递数据。
- 跨层级传递:使用 Context API 或状态管理库(Redux/MobX)。
- 兄弟组件传递:借助共同父组件中转,或使用 Context/状态管理库。
新增:
1. 用过闭包么?什么场景用的?
用过。闭包的核心定义是:函数有权访问其声明时所在的词法作用域,即使函数在该词法作用域之外执行,本质是“函数 + 函数声明时的词法环境”的组合。
常用场景:① 数据私有化/模块化封装:通过闭包隐藏内部变量,避免全局污染,仅暴露指定操作方法。例如:const counter = (() => { let count = 0; return { increment: () => count++, getCount: () => count }; })(); ② 延迟执行与状态保存:如定时器、事件回调中保存外部变量状态,避免循环中变量污染。例如:for (var i = 0; i < 5; i++) { (function(j) { setTimeout(() => console.log(j), 1000); })(i); } ③ 函数防抖/节流:通过闭包保存定时器 ID 或时间戳,实现状态持久化,避免频繁触发函数。
2. 实现一个深拷贝
深拷贝的核心是复制对象的所有层级(包括嵌套对象/数组),确保新对象与原对象完全独立,修改新对象不影响原对象。以下是递归实现的核心方案(支持常见类型):
function deepClone(target) {
// 处理null和基本数据类型(直接返回,无需拷贝)
if (target === null || typeof target !== 'object') {
return target;
}
// 处理日期类型
if (target instanceof Date) {
return new Date(target);
}
// 处理正则类型
if (target instanceof RegExp) {
return new RegExp(target.source, target.flags);
}
// 处理数组和对象(创建新的容器)
const cloneObj = Array.isArray(target) ? [] : {};
// 遍历自身属性(不包含原型链属性)
for (const key in target) {
if (target.hasOwnProperty(key)) {
// 递归拷贝子属性
cloneObj[key] = deepClone(target[key]);
}
}
return cloneObj;
}
补充方案:① 简易方案:JSON.parse(JSON.stringify(target)),优点是简单,缺点是无法拷贝函数、日期、正则、undefined 等;② 成熟方案:使用 Lodash 的_.cloneDeep 方法,支持更多类型,稳定性高。
3. flex 的三个参数是什么?
flex 是 flex-grow、flex-shrink、flex-basis 三个属性的简写属性,语法:flex: [flex-grow] [flex-shrink] [flex-basis]; ,默认值为 0 1 auto。
- flex-grow:定义项目的放大比例,默认值 0(即使容器有剩余空间,项目也不放大)。若所有项目的 flex-grow 总和 >0,剩余空间会按比例分配给各项目。
- flex-shrink:定义项目的缩小比例,默认值 1(容器空间不足时,项目会缩小)。若总和 >0,空间不足时按比例缩小;若为 0,空间不足时不缩小。
- flex-basis:定义项目分配空间前的初始大小,默认值 auto(项目自身实际大小)。可设为具体数值(如 100px)或百分比,优先级高于 width/height。
常用简写:① flex: 1 → 等价于 1 1 0%(均分剩余空间);② flex: auto → 等价于 1 1 auto;③ flex: none → 等价于 0 0 auto(不放大不缩小,保持自身大小)。
4. typeof 检测出的结果都有啥?
typeof 用于检测数据类型,返回值为字符串类型,共 8 种可能结果:
- "undefined":检测 undefined 类型(如 typeof undefined);
- "boolean":检测布尔类型(如 typeof true、typeof false);
- "string":检测字符串类型(如 typeof 'hello'、typeof "");
- "number":检测数字类型(如 typeof 123、typeof NaN、typeof Infinity);
- "bigint":检测大整数类型(如 typeof 10n、typeof 9007199254740991n);
- "symbol":检测 Symbol 类型(如 typeof Symbol('id'));
- "function":检测函数类型(如 typeof function(){}、typeof console.log、typeof class{});
- "object":检测对象、数组、null(如 typeof {}、typeof []、typeof null)。
注意:typeof 无法区分数组、对象、null,需通过 Array.isArray()(判断数组)或 Object.prototype.toString.call()(精准判断类型)进一步区分。
5. vue 跳转路由页面定时器在哪里清除
Vue 中路由跳转时,定时器必须在组件卸载前清除,否则会导致内存泄漏,推荐在 beforeDestroy 或 destroyed 生命周期钩子中处理。
Vue2 实现步骤:
- 在 data 中定义定时器 ID:data() { return { timer: null }; }
- 在 mounted 中创建定时器:mounted() { this.timer = setInterval(() => { console.log('定时器运行'); }, 1000); }
- 在卸载钩子中清除:beforeDestroy() { clearInterval(this.timer); this.timer = null; }
Vue3 组合式 API 实现:
import { onMounted, onUnmounted, ref } from 'vue';
export default {
setup() {
const timer = ref(null);
onMounted(() => {
timer.value = setInterval(() => { console.log('定时器运行'); }, 1000);
});
onUnmounted(() => {
clearInterval(timer.value);
timer.value = null;
});
return {};
}
}
6. 登录模块怎么做的?token 怎么用的?
一、登录模块核心流程
- 前端表单校验:验证用户名、密码非空、格式合法性(如密码长度)等基础规则;
- 请求登录接口:将校验后的用户名、密码提交给后端,后端验证通过后返回 token(用户身份凭证);
- 存储 token:将 token 存入 localStorage(持久化,刷新页面不丢失)或 sessionStorage(会话级,关闭浏览器丢失);
- 路由守卫控制:通过 router.beforeEach 全局路由守卫,判断用户是否登录(是否存在有效 token),未登录则跳转至登录页;
- 登录成功跳转:跳转至首页或之前访问的目标页面(可通过路由 query 参数记录);
- 退出登录:清除存储的 token,跳转至登录页。
二、token 的使用方式
- 请求携带 token:通过 axios 请求拦截器,在所有需要权限的接口请求头中携带 token,格式通常为:headers: { Authorization:
Bearer ${token}}; - token 过期处理:通过 axios 响应拦截器捕获 401 状态码(token 过期/无效),清除本地 token 并跳转至登录页;
- token 刷新机制:若后端支持,可在 token 即将过期时,调用刷新 token 接口获取新 token,更新本地存储的 token,避免用户重新登录。
7. vue 中权限控制怎么设置?
Vue 中权限控制核心是“基于角色的权限控制(RBAC)”,主要分为 4 个层面:
- 路由权限控制:① 路由拦截:通过 router.beforeEach 守卫,判断用户角色是否有权访问目标路由,无权限则跳转至无权限页面;② 动态路由:根据用户权限动态添加可访问路由(如 admin 角色添加管理路由,普通用户不添加)。
- 菜单权限控制:根据用户权限动态渲染菜单,无权限的菜单不显示(如 v-if="hasPermission('menu:user')")。
- 按钮权限控制:通过自定义指令(如 v-permission)控制按钮显示/隐藏,无权限则不渲染或禁用。
- 接口权限控制:后端验证 token 合法性及用户权限,前端通过响应拦截器处理 403(无权限)状态码,跳转至无权限页面。
实现步骤:① 登录后获取用户角色及权限列表,存储到 vuex 或本地;② 路由层面:初始化基础路由,动态添加权限路由;③ 视图层面:通过权限列表控制菜单、按钮渲染;④ 接口层面:依赖后端权限校验,前端辅助拦截。
8. git 流程是什么?怎么解决冲突?
一、常用 Git 工作流程(以 Git Flow 为例)
- 菜单权限控制:根据用户权限动态渲染菜单,无权限的菜单不显示(如 v-if="hasPermission('menu:user')")。
- 克隆远程仓库:git clone 仓库地址;
- 创建开发分支:从 master 分支创建 develop 分支(长期分支),git checkout -b develop master;
- 提交代码:开发完成后,git add . → git commit -m "完成用户模块开发";
- 合并分支:将功能分支合并到 develop,git checkout develop → git merge --no-ff feature/user;
- 功能开发:从 develop 创建功能分支(如 feature/user),git checkout -b feature/user develop;
- 修复 bug:从 master 创建 hotfix 分支(如 hotfix/bug1),修复后合并到 master 和 develop。
- 发布版本:从 develop 创建 release 分支(如 release/1.0.0),测试无误后合并到 master 和 develop;
二、Git 冲突解决方法
- 按钮权限控制:通过自定义指令(如 v-permission)控制按钮显示/隐藏,无权限则不渲染或禁用。
- 接口权限控制:后端验证 token 合法性及用户权限,前端通过响应拦截器处理 403(无权限)状态码,跳转至无权限页面。
实现步骤:① 登录后获取用户角色及权限列表,存储到 vuex 或本地;② 路由层面:初始化基础路由,动态添加权限路由;③ 视图层面:通过权限列表控制菜单、按钮渲染;④ 接口层面:依赖后端权限校验,前端辅助拦截。
8. git 流程是什么?怎么解决冲突?
一、常用 Git 工作流程(以 Git Flow 为例)
- 克隆远程仓库:git clone 仓库地址;
- 创建开发分支:从 master 分支创建 develop 分支(长期分支),git checkout -b develop master;
- 功能开发:从 develop 创建功能分支(如 feature/user),git checkout -b feature/user develop;
- 提交代码:开发完成后,git add . → git commit -m "完成用户模块开发";
- 合并分支:将功能分支合并到 develop,git checkout develop → git merge --no-ff feature/user;
- 发布版本:从 develop 创建 release 分支(如 release/1.0.0),测试无误后合并到 master 和 develop;
- 修复 bug:从 master 创建 hotfix 分支(如 hotfix/bug1),修复后合并到 master 和 develop。
二、Git 冲突解决方法
冲突原因:多人修改同一文件的同一部分,Git 无法自动合并。
- 查看冲突:git pull 或 git merge 时提示冲突,文件中会出现冲突标记(<<<<<<< HEAD(当前分支内容)、=======(分隔线)、>>>>>>> 分支名(待合并分支内容));
- 解决冲突:打开冲突文件,根据需求修改内容(删除冲突标记,保留正确代码);
- 提交解决结果:git add 冲突文件 → git commit -m "解决冲突:合并用户模块代码";
- 后续操作:若在 pull 时解决冲突,直接完成同步;若在 merge 时解决冲突,继续完成合并流程。
注意:解决冲突前先沟通,避免误删他人代码;大型项目建议频繁提交、同步代码,减少冲突概率。
9. git 中 rebase 和 merge 的区别
rebase(变基)和 merge(合并)都是 Git 合并分支的方法,核心区别在于提交历史的处理方式:
| 对比维度 | merge(合并) | rebase(变基) |
|---|---|---|
| 提交历史 | 保留完整提交历史,会生成一个新的合并提交(commit),历史记录呈分叉状 | 改写提交历史,将待合并分支的提交“移植”到目标分支末尾,历史记录呈线性 |
| 冲突处理 | 所有冲突一次性处理,解决后生成合并提交 | 按提交顺序依次处理冲突,每处理一个冲突需执行 git add → git rebase --continue |
| 适用场景 | 团队协作中合并公共分支(如 develop 合并到 master),保留分支开发轨迹 | 个人开发分支同步公共分支(如 feature 分支同步 develop),保持历史记录简洁 |
| 风险 | 无风险,不修改已有提交记录 | 有风险,修改了待合并分支的提交历史,禁止在公共分支(如 develop、master)使用 |
总结:公共分支合并用 merge,个人分支同步公共分支用 rebase。
10. 数组对象中存了 name 和 age 怎么根据 age 进行排序
使用数组的 sort()方法,结合自定义比较函数实现按 age 排序,sort()默认按字符串 Unicode 编码排序,需手动指定数字排序规则:
// 示例数组
const userList = [
{ name: '张三', age: 25 },
{ name: '李四', age: 20 },
{ name: '王五', age: 30 }
];
// 1. 按age升序排序(从小到大)
const ascendingSort = userList.sort((a, b) => a.age - b.age);
console.log(ascendingSort); // 李四(20) → 张三(25) → 王五(30)
// 2. 按age降序排序(从大到小)
const descendingSort = userList.sort((a, b) => b.age - a.age);
console.log(descendingSort); // 王五(30) → 张三(25) → 李四(20)
原理:sort()的比较函数返回值 >0 时,交换 a 和 b 的位置;返回值 <0 时,不交换;返回值=0 时,位置不变。a.age - b.age 实现升序,b.age - a.age 实现降序。
11. 数组对象如何去重
数组对象去重核心是根据唯一标识(如 id)判断重复,常用 3 种方法:
方法 1:利用 Map(推荐,高效)
const arr = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 1, name: '张三' } // 重复
];
const uniqueArr = Array.from(new Map(arr.map(item => [item.id, item])).values());
console.log(uniqueArr); // 保留第一个重复项
方法 2:利用 filter + findIndex
const uniqueArr = arr.filter((item, index) => {
// 只保留第一次出现的id对应的元素
return arr.findIndex(obj => obj.id === item.id) === index;
});
方法 3:利用 Set(需先转唯一键)
const idSet = new Set();
const uniqueArr = [];
for (const item of arr) {
if (!idSet.has(item.id)) {
idSet.add(item.id);
uniqueArr.push(item);
}
}
说明:以上方法均按 id 去重,若需按其他字段(如 name),只需将 item.id 改为对应字段即可;默认保留第一个重复项,若需保留最后一个,可反向遍历数组。
12. 防抖和节流在哪些场景用过,怎么实现
一、防抖(debounce)
核心逻辑:触发事件后,延迟 n 秒执行函数;若 n 秒内再次触发,重新计时。
适用场景:① 搜索框输入联想(避免输入过程中频繁请求接口);② 窗口 resize 事件(避免窗口调整时频繁触发计算);③ 按钮点击防重复提交(避免快速点击多次触发)。
实现代码:
function debounce(fn, delay) {
let timer = null;
return function(...args) {
// 清除之前的定时器,重新计时
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args); // 绑定this和参数
}, delay);
};
}
二、节流(throttle)
核心逻辑:触发事件后,n 秒内只执行一次函数,避免频繁执行。
适用场景:① 滚动事件(如滚动加载更多、监听滚动位置);② 鼠标移动事件(如拖拽时获取位置);③ 高频点击事件(如游戏射击按钮)。
实现代码(时间戳版):
function throttle(fn, interval) {
let lastTime = 0; // 上一次执行时间
return function(...args) {
const now = Date.now();
// 若当前时间 - 上一次执行时间 > 间隔,执行函数
if (now - lastTime > interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
补充:节流还有定时器版,核心是触发时执行一次,然后设置定时器,n 秒内不重复执行;时间戳版立即执行,定时器版延迟执行,可根据场景选择。
13. 前端怎么做性能优化
前端性能优化从“加载优化、渲染优化、运行时优化”三个核心维度入手:
- 加载优化:① 资源压缩与合并(JS/CSS 压缩、图片压缩);② 资源缓存(设置 HTTP 缓存 Cache-Control/Expires、使用 ETag);③ 懒加载(图片懒加载、组件懒加载、路由懒加载);④ 预加载/预连接(preload 关键资源、preconnect 第三方域名);⑤ 减少 HTTP 请求(合并文件、使用 Sprite 精灵图);⑥ 使用 CDN 加速(静态资源部署到 CDN)。
- 渲染优化:① 减少重排(Reflow)和重绘(Repaint)(避免频繁操作 DOM、使用 CSS3 硬件加速 transform/opacity);② 优化 CSS 选择器(避免复杂选择器、减少嵌套);③ 避免阻塞渲染(JS 放 body 底部、CSS 用 link 标签而非 @import);④ 使用虚拟 DOM(React/Vue)减少真实 DOM 操作;⑤ 合理使用 requestAnimationFrame 代替 setTimeout。
- 运行时优化:① 代码层面(减少闭包使用、避免内存泄漏、优化循环逻辑);② 状态管理优化(避免不必要的状态更新、使用 memo/useMemo 缓存组件/计算结果);③ 大数据渲染优化(虚拟列表、分页加载);④ 避免频繁 GC(减少临时变量创建)。
辅助优化:① 性能监控(使用 Lighthouse、Chrome DevTools 分析性能瓶颈);② 服务端优化(SSR 服务端渲染、SSG 静态站点生成,提升首屏加载速度)。
14. webpack 用过吗?流程是什么?常用的 plugin 和 loader 都有啥?打包后文件过大怎么办?
一、Webpack 使用经验
用过。Webpack 是前端模块化打包工具,核心作用是将分散的模块化文件(JS、CSS、图片等)打包为浏览器可识别的静态资源,同时提供代码转换、优化等功能。
二、Webpack 打包流程
- 初始化:读取 webpack.config.js 配置,合并默认配置,创建 Compiler 对象;
- 编译:从 Entry 入口文件开始,解析模块依赖,通过 Loader 转换非 JS 资源,生成 AST 抽象语法树,收集依赖关系,构建依赖树;
- 优化:代码分割(拆分 chunk)、Tree-shaking(剔除死代码)、模块合并等;
- 生成:将优化后的模块整合为 chunk,注入运行时代码,生成最终静态资源;
- 输出:将静态资源写入指定目录,完成打包。
三、常用 Plugin 和 Loader
- 常用 Loader:① babel-loader:将 ES6+ 代码转换为 ES5;② css-loader:解析 CSS 文件,处理 CSS 依赖;③ style-loader:将 CSS 注入到 HTML 的 style 标签;④ file-loader:处理图片、字体等资源,输出为单独文件;⑤ url-loader:小图片转 Base64,减少请求;⑥ sass-loader:解析 SCSS/SASS 文件。
- 常用 Plugin:① HtmlWebpackPlugin:自动生成 HTML 文件,引入打包后的资源;② MiniCssExtractPlugin:将 CSS 提取为单独文件(替代 style-loader);③ TerserPlugin:压缩 JS 代码;④ CleanWebpackPlugin:打包前清空输出目录;⑤ DefinePlugin:注入环境变量(如 process.env.NODE_ENV);⑥ CopyWebpackPlugin:复制静态资源到输出目录。
四、打包后文件过大的解决方法
- 代码分割:① 路由分割(React.lazy/Vue 异步组件);② 公共模块提取(splitChunks 提取第三方库如 lodash、react);
- 资源优化:① 压缩代码(TerserPlugin/MiniCssExtractPlugin 压缩);② 图片优化(压缩图片、使用 WebP 格式);③ 剔除无用代码(Tree-shaking,需开启 mode: 'production');
- 第三方库优化:① 使用 CDN 引入第三方库(如 React、Vue),避免打包进 bundle;② 替换体积大的库(如用 lodash-es 替代 lodash,支持 Tree-shaking);
- 其他:① 开启 Gzip/Brotli 压缩(服务端配置);② 减少不必要的依赖,按需引入(如 Element UI 按需引入)。
15. vue3 和 vue2 的区别
| 对比维度 | Vue2 | Vue3 |
|---|---|---|
| 核心架构 | 选项式 API(Options API),按 data、methods、computed 等组织代码 | 组合式 API(Composition API),按逻辑功能组织代码,更灵活 |
| 响应式原理 | Object.defineProperty,监听属性的 get/set,无法监听数组索引、对象新增属性 | Proxy,代理整个对象,支持监听数组索引、对象新增/删除属性,性能更好 |
| 生命周期 | 选项式生命周期(如 created、mounted) | 组合式 API 生命周期(如 onMounted、onUnmounted),需手动导入 |
| 模板语法 | 支持 HTML 模板,JSX 需额外配置 | 原生支持 JSX,模板语法新增 Teleport、Suspense 等 |
| 状态管理 | Vuex(核心是 Mutation/Action) | Pinia(简化版 Vuex,无需 Mutation,支持 TypeScript),Vuex4 兼容 Vue3 |
| TypeScript 支持 | 支持有限,需额外配置 vue-class-component | 原生支持 TypeScript,类型推断更完善 |
| 性能 | 重渲染性能一般,打包体积较大 | 重渲染性能提升,打包体积更小(Tree-shaking 优化) |
| 其他 | 无碎片组件,自定义指令钩子不同 | 支持碎片组件(多个根节点),自定义指令钩子优化,新增 setup 入口 |
16. es6 常用的都有哪些
- let/const 声明:替代 var,let 支持块级作用域,const 声明常量(不可修改引用);
- 箭头函数:简化函数写法,不绑定 this(this 指向外层词法作用域),如(a, b) => a + b;
- 模板字符串:用
包裹,支持换行和变量插值${},如Hello ${name}`; - 解构赋值:快速提取数组/对象中的值,如 const [a, b] = [1, 2],const { name } = { name: '张三' };
- 扩展运算符(...):展开数组/对象,如[...arr1, ...arr2],{ ...obj1, name: '李四' };
- 默认参数:函数参数设置默认值,如 function fn(a = 1) {};
- 剩余参数(...rest):收集剩余参数为数组,如 function fn(...args) {};
- 数组方法:map(映射)、filter(过滤)、reduce(累加)、forEach(遍历)、find(查找)、some(是否存在)、every(是否全部满足);
- Promise:处理异步操作,解决回调地狱,如 new Promise((resolve, reject) => {});
- class 类:语法糖,替代原型链继承,如 class Person { constructor(name) { this.name = name; } };
- import/export:模块化导入导出,替代 CommonJS 的 require/module.exports;
- Set/Map 数据结构:Set 存储唯一值,Map 存储键值对(键可任意类型)。
17. css3 新增了哪些
- 选择器:① 属性选择器(如[attr^=value]、[attr$=value]);② 伪类选择器(如:nth-child()、:hover、:active、:focus、:not());③ 伪元素选择器(如::before、::after、::first-line、::selection);
- 盒模型与布局:① Flex 布局(弹性布局);② Grid 布局(网格布局);③ 多列布局(column-count/column-gap);
- 边框与背景:① 圆角(border-radius);② 阴影(box-shadow、text-shadow);③ 背景渐变(linear-gradient、radial-gradient);④ 多背景图(background-image 多图叠加);⑤ 背景大小(background-size);
- 动画与过渡:① 过渡(transition,实现平滑动画);② 动画(animation,自定义关键帧动画);③ 变换(transform,如 rotate 旋转、scale 缩放、translate 平移、skew 倾斜);
- 文本相关:① 文本溢出(text-overflow: ellipsis);② 文本阴影(text-shadow);③ 字体(@font-face 引入自定义字体);④ 换行(word-wrap/word-break);
- 其他:① 透明度(opacity);② 滤镜(filter,如 blur 模糊、grayscale 灰度);③ 媒体查询(@media,响应式布局基础);④ 变量(--var 定义变量,var()使用)。
18. 在 vue 中,data 中有个变量 a,在 created 里面有个定时器,在定时器里 this.a 能访问到这个变量么?如果不能为什么?怎么解决?
能访问到。
原因:Vue 的 created 生命周期钩子执行时,组件实例已创建完成,data 中的数据已被初始化并挂载到组件实例(this)上。定时器函数是在 created 内部定义的,形成闭包,有权访问外部的 this(组件实例),因此可以通过 this.a 访问到 data 中的变量 a。
示例代码:
new Vue({
data() {
return { a: 10 };
},
created() {
setInterval(() => {
console.log(this.a); // 能正常访问,输出10
this.a++; // 也能正常修改
}, 1000);
}
})
补充:若定时器函数是普通函数(非箭头函数),this 会指向 window(非严格模式),此时无法访问 this.a。解决方法:① 用箭头函数(绑定外层 this);② 在 created 中保存 this 到变量(如 const _this = this; 定时器中用_this.a);③ 用 bind 绑定 this(setInterval(function() {}).bind(this), 1000))。
19. vuex 怎么解决刷新丢失问题?
Vuex 状态存储在内存中,页面刷新时组件实例重建,内存释放,状态丢失。解决核心思路是“状态持久化”,将 Vuex 状态同步到本地存储(localStorage/sessionStorage),刷新后从本地存储恢复状态。
常用实现方法:
- 手动实现(简单场景):① 存储:在 mutation 中,每次修改状态后同步到本地存储;② 恢复:在 Vuex 初始化时(如 created 钩子或 store 创建时),从本地存储读取状态并赋值给 state。示例:
// store/index.js const store = new Vuex.Store({ state: { userInfo: localStorage.getItem('userInfo') ? JSON.parse(localStorage.getItem('userInfo')) : null }, mutations: { SET_USER_INFO(state, userInfo) { state.userInfo = userInfo; // 同步到localStorage localStorage.setItem('userInfo', JSON.stringify(userInfo)); } } }); - 使用第三方插件(复杂场景,推荐):使用 vuex-persistedstate 插件,自动实现状态持久化,无需手动同步。示例:
// 安装:npm install vuex-persistedstate --save import createPersistedState from 'vuex-persistedstate'; const store = new Vuex.Store({ // ...其他配置 plugins: [createPersistedState({ storage: localStorage, // 存储方式:localStorage/sessionStorage reducer: (state) => ({ userInfo: state.userInfo }) // 只持久化userInfo字段 })] });
注意:① 若状态中有敏感数据(如 token),不建议用 localStorage(明文存储),可加密存储或用 sessionStorage(会话级);② 本地存储存储容量有限(约 5MB),避免存储过大状态。
20. 实现一个布局,第一个 div 占一份,第二个占两份,第三个占三份
推荐用 Flex 布局实现,简洁高效,兼容性好:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Flex比例布局</title>
<style>
.container {
display: flex; /* 开启Flex布局 */
width: 100%;
height: 300px; /* 父容器高度,可自定义 */
gap: 10px; /* 子元素间距,可选 */
}
.item1 {
flex: 1; /* 占1份 */
background-color: #f00;
}
.item2 {
flex: 2; /* 占2份 */
background-color: #0f0;
}
.item3 {
flex: 3; /* 占3份 */
background-color: #00f;
}
</style>
</head>
<body>
<div class="container">
<div class="item1"></div>
<div class="item2"></div>
<div class="item3"></div>
</div>
</body>
</html>
补充:也可用 Grid 布局实现:
.container {
display: grid;
grid-template-columns: 1fr 2fr 3fr; /* 列比例1:2:3 */
width: 100%;
height: 300px;
gap: 10px;
}
说明:1fr 表示“剩余空间的一份”,flex: n 等价于 flex: n 1 0%,会按比例分配父容器的剩余空间;Grid 布局的 grid-template-columns 直接定义列比例,更直观。
21. 二次封装过 element ui 吗?为什么?
封装过。
核心原因:① 统一业务风格:Element UI 组件是通用的,二次封装可统一项目内组件的样式、交互逻辑(如按钮大小、表单校验规则、弹窗样式),避免重复开发;② 适配业务需求:通用组件无法满足特定业务场景(如自定义表格列、表单联动逻辑),二次封装可扩展功能;③ 降低维护成本:若后续需替换 UI 库或修改组件逻辑,只需修改封装后的组件,无需修改所有使用处;④ 简化使用:封装后可减少重复属性传递(如 el-button 默认设置 type="primary"),简化代码。
示例(封装 el-button):
<template>
<el-button
:type="type || 'primary'"
:size="size || 'medium'"
:loading="loading"
@click="handleClick"
>
<slot></slot>
</el-button>
</template>
<script>
export default {
name: 'MyButton',
props: {
type: String,
size: String,
loading: Boolean
},
methods: {
handleClick(e) {
// 扩展业务逻辑(如按钮点击日志)
console.log('按钮点击');
this.$emit('click', e); // 透传事件
}
}
}
</script>
22. vue 响应拦截器是干啥的?
Vue 中响应拦截器是 axios 的核心功能,用于“统一处理所有接口的响应数据和错误”,在后端返回响应后、前端业务代码接收数据前执行,核心作用是简化业务代码、统一错误处理。
核心功能:
- 统一处理响应数据:① 过滤无用数据(如后端返回{ code: 200, data: {}, msg: '' },拦截器中直接返回 data,业务代码无需重复解析);② 格式化数据(如日期格式化、数字格式化)。
- 统一处理错误:① 网络错误(如请求超时、断网);② 业务错误(如 code≠200、token 过期 401、无权限 403);③ 错误提示(统一弹框提示错误信息,避免业务代码重复写弹框)。
- 其他扩展:① 刷新 token(捕获 401 状态码,调用刷新 token 接口后重新发起原请求);② 日志记录(记录接口响应时间、错误信息)。
示例代码:
import axios from 'axios';
const service = axios.create({ baseURL: '/api' });
// 响应拦截器
service.interceptors.response.use(
(response) => {
// 成功响应:直接返回data
const res = response.data;
return res.code === 200 ? res.data : Promise.reject(res.msg);
},
(error) => {
// 错误处理
if (error.response) {
const status = error.response.status;
if (status === 401) {
// token过期:清除token,跳转登录页
localStorage.removeItem('token');
window.location.href = '/login';
} else if (status === 403) {
alert('无权限访问');
}
} else {
alert('网络错误,请检查网络');
}
return Promise.reject(error);
}
)
23. 在哪里使用过自定义指令?
自定义指令在 Vue 项目中常用于封装复用性强的 DOM 操作逻辑,核心使用场景集中在“元素行为控制”和“视图交互增强”,具体场景及示例如下:
- **权限控制(按钮/元素显示隐藏)**:根据用户权限动态控制元素是否渲染或禁用,替代重复的 v-if 判断。示例:封装 v-permission 指令,传入权限标识,无权限则移除元素。
- 表单输入增强:限制输入格式(如仅允许输入数字、限制输入长度)、自动聚焦、输入防抖等。示例:封装 v-input-number 指令,禁止输入非数字字符。
- 元素交互效果:实现自定义的悬停效果、点击反馈、滚动动画等。示例:封装 v-hover-effect 指令,鼠标悬浮时添加元素缩放、阴影变化效果。
- 资源加载处理:图片加载失败时显示默认图、动态加载脚本/样式等。示例:封装 v-img-error 指令,图片加载失败时替换为默认占位图。
- 其他 DOM 操作:如自动复制文本、限制元素拖拽范围、自定义滚动条等。
示例代码(v-permission 指令):
// 全局注册指令(main.js)
Vue.directive('permission', {
inserted: function(el, binding) {
// 获取用户权限列表(假设从vuex或本地存储获取)
const userPermissions = store.state.permissions;
// 若用户无该权限,移除元素
if (!userPermissions.includes(binding.value)) {
el.parentNode.removeChild(el);
}
}
});
// 组件中使用
// <button v-permission="['user:delete']">删除用户</button>
24. 简述数组方法 map,filter,forEach
三者均为数组遍历方法,核心作用是迭代数组元素,但功能定位和返回值不同,具体区别如下:
- map:① 核心功能:遍历数组,对每个元素执行回调函数,返回一个新数组(新数组元素为回调函数的返回值);② 不改变原数组;③ 适用场景:数组元素转换(如格式转换、值计算)。示例:将数组中数字翻倍:[1,2,3].map(num => num * 2) → 结果:[2,4,6];
- map:① 核心功能:遍历数组,对每个元素执行回调函数,返回一个新数组(新数组元素为回调函数的返回值);② 不改变原数组;③ 适用场景:数组元素转换(如格式转换、值计算)。示例:将数组中数字翻倍:[1,2,3].map(num => num * 2) → 结果:[2,4,6];
- filter:① 核心功能:遍历数组,筛选出符合回调函数条件(返回 true)的元素,组成新数组返回;② 不改变原数组;③ 适用场景:数组元素筛选(如筛选符合条件的数据)。示例:筛选数组中的偶数:[1,2,3,4].filter(num => num % 2 === 0) → 结果:[2,4];
- forEach:① 核心功能:遍历数组,对每个元素执行回调函数,无返回值(返回 undefined);② 不改变原数组(若回调内手动修改元素属性则可能改变);③ 适用场景:单纯的数组遍历(如打印元素、批量执行操作)。示例:遍历打印数组元素:[1,2,3].forEach(num => console.log(num)) → 依次打印 1、2、3;
25. 数组方法 sort 默认排序方式是什么?
sort()方法默认排序方式是按字符串的 Unicode 编码(UTF-16)顺序排序,而非数字大小顺序。
核心特点:
- 排序前会将数组元素统一转为字符串(即使是数字类型),再比较字符串的 Unicode 编码;
- 数字排序陷阱:若直接对数字数组使用 sort(),会出现不符合预期的结果。例如:[10, 2, 22, 1].sort() → 结果:[1, 10, 2, 22],原因是转为字符串后"10"的 Unicode 编码小于"2";
- 解决方法:需传入自定义比较函数,实现数字大小排序。示例:
// 数字升序排序 [10, 2, 22, 1].sort((a, b) => a - b); // 结果:[1, 2, 10, 22] // 数字降序排序 [10, 2, 22, 1].sort((a, b) => b - a); // 结果:[22, 10, 2, 1]
26. 获取时间戳怎么获取?
时间戳是指从 1970 年 1 月 1 日 00:00:00 UTC 到当前时间的毫秒数,常用获取方法有 4 种:
- **Date.now()**:ES6 新增,最简洁高效,直接返回当前时间戳(毫秒),无兼容性问题。示例:const timestamp = Date.now(); // 输出:1755088823456;
- **new Date().getTime()**:创建 Date 实例后调用 getTime()方法,兼容性好(支持 ES5 及以下)。示例:const timestamp = new Date().getTime();;
- **new Date().valueOf()**:与 getTime()效果一致,返回当前时间戳(毫秒)。示例:const timestamp = new Date().valueOf();;
- **+new Date()**:通过一元加号运算符将 Date 实例转为数字类型,即时间戳(毫秒),写法简洁。示例:const timestamp = +new Date();;
补充:若需获取秒级时间戳(后端常用),只需对以上结果除以 1000 并取整:const secondTimestamp = Math.floor(Date.now() / 1000);。
27. 如何判断一个对象是数组?
常用 5 种判断方法,各有优劣,推荐使用前 2 种:
- **Array.isArray(obj)**:ES6 新增,最推荐,精准判断,兼容性好(IE9 及以上)。示例:
Array.isArray([]); // true Array.isArray({}); // false Array.isArray(null); // false - **Object.prototype.toString.call(obj) === '[object Array]'**:最精准,兼容性极佳(支持所有浏览器),可区分数组、对象、null 等。示例:
Object.prototype.toString.call([]); // "[object Array]" Object.prototype.toString.call({}); // "[object Object]" Object.prototype.toString.call(null); // "[object Null]" - obj instanceof Array:判断原型链,存在局限性:若数组在不同 iframe 中创建,原型链不同,会判断为 false。示例:[] instanceof Array; // true;
- obj.constructor === Array:通过构造函数判断,局限性:若手动修改 obj.constructor,会导致判断失效。示例:[].constructor === Array; // true;
- typeof obj === 'object' && obj.length !== undefined:不推荐,存在漏洞(如字符串、类数组对象也可能满足条件)。示例:typeof [] === 'object' && [].length !== undefined; // true,但 typeof 'abc' === 'string' 不满足,类数组对象如 arguments 会误判。
28. js 中如何去除空格?
根据空格位置(首尾、所有、指定位置),常用方法如下:
- **去除首尾空格(最常用)**: trim():ES5 新增,去除字符串首尾的空格(包括空格、制表符\t、换行符\n 等空白字符),不改变原字符串。示例:' hello world '.trim() → "hello world";
- trimStart()/trimLeft():去除首部空格;trimEnd()/trimRight():去除尾部空格。示例:' hello '.trimStart() → "hello ";
- 去除所有空格: 正则表达式:str.replace(/\s+/g, ''),\s 匹配所有空白字符,g 表示全局匹配。示例:'he l lo w orld'.replace(/\s+/g, '') → "helloworld";
- 去除指定位置空格:通过正则精准匹配,例如只去除中间空格:str.replace(/(\S)\s+(\S)/g, '$1$2')。示例:'he l lo'.replace(/(\S)\s+(\S)/g, '$1$2') → "hello";
补充:若需处理数组中元素的空格,可结合 map 和 trim():const arr = [' a ', 'b ', ' c']; const newArr = arr.map(item => item.trim()); → ["a", "b", "c"]。
29. 常见的 css 兼容都有哪些?怎么解决?
常见 CSS 兼容问题主要集中在旧浏览器(如 IE6-9)和不同内核浏览器(Chrome、Firefox、Safari),核心解决思路是“添加浏览器前缀”“降级处理”“特性检测”:
- **CSS3 属性兼容(如 transition、transform、border-radius)**: 问题:旧浏览器不支持 CSS3 属性,需添加浏览器私有前缀。解决:添加-webkit-(Chrome、Safari)、-moz-(Firefox)、-ms-(IE)、-o-(Opera)前缀。示例:
.box { -webkit-border-radius: 8px; /* Chrome、Safari */ -moz-border-radius: 8px; /* Firefox */ -ms-border-radius: 8px; /* IE */ -o-border-radius: 8px; /* Opera */ border-radius: 8px; /* 标准语法 */ -webkit-transform: rotate(30deg); transform: rotate(30deg); }工具优化:使用 Autoprefixer 自动添加前缀,无需手动编写。 - 盒模型兼容: 问题:IE6-7 使用怪异盒模型(width 包含 padding 和 border),标准浏览器使用标准盒模型。解决:通过 box-sizing 统一盒模型。示例:
{ -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; /* 统一为怪异盒模型,width=内容+padding+border */ } - 浮动清除兼容: 问题:IE6-7 浮动后父元素塌陷,且不支持:after 伪元素清除浮动。解决:① 标准浏览器:使用 clearfix 伪类;② IE6-7:添加 zoom: 1 触发 hasLayout。示例:
.clearfix:after { content: ""; display: block; clear: both; visibility: hidden; height: 0; } .clearfix { zoom: 1; /* 兼容IE6-7 */ } - inline-block 兼容: 问题:IE6-7 不支持 inline-block 属性(对块级元素无效)。解决:对 IE6-7 添加 display: inline; zoom: 1。示例:
.inline-box { display: inline-block; *display: inline; /* IE6-7专用 */ *zoom: 1; /* 触发hasLayout */ } - 透明度兼容: 问题:IE6-8 不支持 opacity 属性,使用 filter 滤镜。解决:
.transparent { opacity: 0.5; /* 标准语法 */ filter: alpha(opacity=50); /* IE6-8,值为0-100 */ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; /* 更规范的IE写法 */ }
30. es5 的继承有哪些缺点?es6 为什么没有这些缺点?
一、ES5 继承的核心缺点(以原型链继承 + 构造函数继承为例)
- 原型链继承缺点:① 父类原型的引用类型属性会被所有子类实例共享(修改一个实例的属性会影响其他实例);② 子类实例创建时无法向父类构造函数传递参数;
- 构造函数继承缺点:① 无法继承父类原型上的方法和属性(只能继承父类构造函数内的属性和方法),导致方法复用性差(每个实例都有独立的方法副本);② 父类构造函数会被调用多次,效率低;
- 组合继承(原型链 + 构造函数)缺点:虽解决了上述部分问题,但父类构造函数会被调用两次(一次是创建子类原型时,一次是创建子类实例时),导致子类原型上存在多余的父类属性。
二、ES6 Class 继承解决上述缺点的原因
ES6 的 class 继承基于 ES5 的原型链,但语法糖封装更完善,底层优化了继承逻辑:
- 解决引用类型共享问题:ES6 通过 constructor 构造函数初始化实例属性,每个实例的属性独立,原型上的方法共享,避免引用类型属性共享;
- 支持向父类传递参数:通过 super()调用父类构造函数,可在子类构造函数中向父类传递参数。示例:class Child extends Parent { constructor(name) { super(name); } };
- 高效继承原型方法:子类通过 extends 继承父类的原型方法和属性,无需重复定义,方法复用性强,且父类构造函数仅调用一次(子类实例创建时通过 super()调用);
- 清晰的继承层级:class 语法更直观,继承关系明确,避免了 ES5 原型链继承的复杂操作(如手动设置子类原型为父类实例)。
示例对比:ES5 组合继承需手动处理原型和构造函数,ES6 class 只需 extends 和 super 即可实现完整继承。
31. ajax 和 fetch 有什么区别?fetch 的特点是什么?
一、Ajax 和 Fetch 的核心区别
| 对比维度 | Ajax(XMLHttpRequest) | Fetch(ES6 新增) |
|---|---|---|
| 语法 | 语法繁琐,需手动创建 XHR 对象、绑定事件、处理状态 | 语法简洁,基于 Promise,支持链式调用(.then()/.catch()) |
| 异步处理 | 早期依赖回调函数,易产生回调地狱;可结合 Promise 封装 | 原生支持 Promise,可结合 async/await 进一步简化异步代码 |
| 错误处理 | 网络错误触发 onerror 事件,HTTP 错误(如 404、500)需手动判断 status | 网络错误会 reject,但 HTTP 错误(404、500)不会 reject,需手动判断 response.ok |
| 默认行为 | 默认不携带 Cookie,需手动设置 withCredentials: true | 默认也不携带 Cookie,需设置 credentials: 'include' |
| 终止请求 | 支持 abort()方法终止请求 | 需结合 AbortController 实现请求终止 |
| 兼容性 | 兼容性好(支持所有主流浏览器,包括 IE6+) | IE 不支持,需 polyfill 兼容旧浏览器 |
二、Fetch 的核心特点
- 基于 Promise:原生支持 Promise,避免回调地狱,可结合 async/await 写出同步风格的异步代码;
- 语法简洁:一行代码即可发起请求,如 fetch('/api/data'),相比 Ajax 的多步操作更简洁;
- 模块化设计:将请求和响应分离为 Request、Response 对象,可灵活配置请求头、请求体、响应处理等;
- 支持流式处理:可处理大文件(如视频、音频)的流式传输,无需等待整个文件加载完成;
- 默认不携带 Cookie:需手动设置 credentials: 'include'才能携带 Cookie,增强安全性;
- HTTP 错误不 reject:只有网络错误(如断网、跨域失败)会触发 reject,404、500 等 HTTP 错误需通过 response.ok 判断。
32. 什么是 BFC?BFC 的原理是什么?
一、BFC 定义
BFC(Block Formatting Context,块级格式化上下文)是 CSS 中的一种渲染机制,可理解为“一个独立的渲染区域”,区域内的元素渲染规则不受外部影响,同时也不会影响外部元素。
二、BFC 的触发条件(满足任一即可)
- 根元素(html);
- 浮动元素(float: left/right,不包括 none);
- 绝对定位/固定定位元素(position: absolute/fixed);
- 行内块元素(display: inline-block);
- overflow 值不为 visible 的块元素(overflow: hidden/auto/scroll);
- flex 容器(display: flex/inline-flex);
- grid 容器(display: grid/inline-grid)。
三、BFC 的核心原理(渲染规则)
- 区域内元素垂直排列:BFC 内的块级元素会沿垂直方向依次排列,每个元素的 margin-top 和 margin-bottom 会发生重叠(margin 塌陷);
- 独立的渲染环境:BFC 内的元素渲染不会影响外部元素,外部元素也不会影响内部;
- margin 重叠解决:两个相邻的块级元素若都处于不同的 BFC 中,它们的 margin 不会重叠;
- 清除浮动影响:BFC 会包含内部的浮动元素(即父元素触发 BFC 后,不会因内部浮动元素而塌陷);
- 阻止元素被浮动元素覆盖:BFC 区域不会与浮动元素的区域重叠(可用于实现两栏布局)。
四、BFC 的应用场景
- 解决父元素浮动塌陷;
- 解决 margin 重叠问题;
- 实现两栏布局(左侧浮动,右侧触发 BFC 不被覆盖);
- 阻止文字环绕浮动元素。
33. vue 怎么确定事件源?
Vue 中确定事件源(即触发事件的 DOM 元素),核心是通过**事件对象(event)**的相关属性获取,常用方法有 3 种:
- event.target:获取实际触发事件的元素(可能是子元素,若事件委托场景)。示例:
<template> <div @click="handleClick" class="parent"> 父元素 <button class="child">子按钮</button> </div> </template> <script> export default { methods: { handleClick(e) { console.log(e.target); // 点击子按钮时输出button元素,点击父元素其他区域输出div元素 } } } </script> - event.currentTarget:获取绑定事件的元素(即 @click 绑定的元素,固定不变)。示例:上述代码中,无论点击子按钮还是父元素其他区域,e.currentTarget 都输出 div 元素;
- 通过$event 传递参数,结合 ref 获取元素:若需精准定位特定元素,可给元素添加 ref,通过 ref 获取 DOM 元素,结合事件对象确认。示例:
<template> <button ref="btn1" @click="handleBtn($event)">按钮1</button> <button ref="btn2" @click="handleBtn($event)">按钮2</button> </template> <script> export default { methods: { handleBtn(e) { if (e.target === this.$refs.btn1) { console.log("触发按钮1"); } else if (e.target === this.$refs.btn2) { console.log("触发按钮2"); } } } } </script>
补充:Vue3 组合式 API 中,可通过 ref()获取 DOM 元素,事件对象的使用方式与 Vue2 一致。
34. 怎么解决跨域?分别有什么弊端?
跨域是指浏览器因“同源策略”限制,禁止不同协议、域名、端口的页面之间相互请求资源。常用解决方法及弊端如下:
- **JSONP(JSON with Padding)**: 原理:利用 script 标签不受同源策略限制的特性,通过动态创建 script 标签,请求后端接口并指定回调函数,后端将数据包裹在回调函数中返回,前端执行回调获取数据。弊端:① 仅支持 GET 请求,不支持 POST、PUT 等其他请求方式;② 存在安全风险(可能遭受 XSS 攻击,需验证后端返回的回调函数名);③ 无法捕获请求错误(如 404、500)。
- **CORS(跨域资源共享,后端配置)**: 原理:后端在响应头中添加 Access-Control-Allow-Origin 等字段,告知浏览器允许指定域名的跨域请求。弊端:① 需后端配合配置,前端无法单独实现;② 复杂请求(如 POST、带自定义请求头)会触发预检请求(OPTIONS),增加网络开销;③ 旧浏览器(如 IE8-9)不支持 CORS,需兼容处理。
- **代理服务器(前端配置,如 Vue CLI 代理、Nginx 代理)**: 原理:利用服务器端不受同源策略限制的特性,前端请求本地代理服务器,代理服务器转发请求到目标后端服务器,再将响应结果返回给前端。弊端:① 需配置代理服务器,增加部署复杂度;② 仅适用于开发环境或可控制的服务器环境;③ 若代理服务器配置不当,可能引发安全风险(如反向代理泄露内部接口)。
- **document.domain + iframe(主域名相同,子域名不同场景)**: 原理:将两个页面的 document.domain 设置为相同的主域名(如 a.test.com 和 b.test.com 都设置为 test.com),实现跨域通信。弊端:① 仅适用于主域名相同、子域名不同的场景,局限性大;② 只能实现页面间通信,无法直接解决接口请求跨域;③ 存在安全风险(同主域名下的其他子域名可共享资源)。
- **postMessage(页面间跨域通信)**: 原理:通过 window.postMessage()方法向其他窗口发送消息,目标窗口通过监听 message 事件接收消息,实现跨域通信。弊端:① 仅适用于页面间通信(如 iframe、新窗口),不适合接口请求跨域;② 需严格验证消息来源(origin),否则可能遭受 XSS 攻击;③ 兼容性依赖浏览器支持。
推荐方案:开发环境用代理服务器,生产环境用 CORS。
35. 图片懒加载怎么实现?
图片懒加载(Lazy Loading)核心是“只加载可视区域内的图片”,减少初始加载资源,提升页面性能。常用实现方法有 2 种:
- **原生方法(IntersectionObserver API,推荐)**: 原理:利用 IntersectionObserver 监听图片元素是否进入可视区域,进入后再设置图片的 src 属性加载图片。实现步骤:
<!-- 1. 页面初始化时,图片src设为占位图,真实地址存放在data-src属性中 --> <img class="lazy-img" src="placeholder.jpg" data-src="real1.jpg" alt="图片1"> <img class="lazy-img" src="placeholder.jpg" data-src="real2.jpg" alt="图片2"> <script> // 2. 创建IntersectionObserver实例,监听元素可见性 const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { // 3. 元素进入可视区域 if (entry.isIntersecting) { const img = entry.target; // 4. 将data-src赋值给src,加载真实图片 img.src = img.dataset.src; // 5. 图片加载完成后,停止监听该元素 observer.unobserve(img); } }); }); // 6. 监听所有懒加载图片 document.querySelectorAll('.lazy-img').forEach(img => { observer.observe(img); }); </script>优点:性能好(浏览器原生 API,异步监听,不阻塞主线程),无需手动计算滚动位置;缺点:IE 不支持,需 polyfill 兼容。 - **传统方法(监听 scroll 事件)**: 原理:监听 window 的 scroll 事件,滚动时计算图片元素的位置与可视区域的关系,若图片进入可视区域,则加载图片。实现步骤:
<img class="lazy-img" src="placeholder.jpg" data-src="real1.jpg" alt="图片1"> <script> // 计算元素是否进入可视区域 function isInViewport(img) { const rect = img.getBoundingClientRect(); // 元素顶部小于视口高度,且元素底部大于0(进入可视区域) return rect.top < window.innerHeight && rect.bottom > 0; } // 加载可视区域内的图片 function loadLazyImg() { document.querySelectorAll('.lazy-img').forEach(img => { if (isInViewport(img) && !img.src.includes('real')) { img.src = img.dataset.src; } }); } // 监听scroll事件(结合节流优化性能) window.addEventListener('scroll', throttle(loadLazyImg, 100)); // 页面初始化时加载一次 loadLazyImg(); </script>优点:兼容性好(支持所有浏览器);缺点:scroll 事件触发频繁,需结合节流优化,否则影响页面性能。
补充:Vue 项目中可封装为自定义指令(v-lazy),实现图片懒加载的复用。
36. 为什么 rem 能实现移动端布局?
rem(font size of the root element)是相对单位,含义是“相对于根元素(html)的字体大小”。其能实现移动端布局的核心原因是可通过动态修改根元素字体大小,实现页面元素的等比例缩放,适配不同屏幕尺寸的移动端设备。
具体原理和实现逻辑:
- 相对根元素字体大小:rem 的计算基准是 html 标签的 font-size。例如:若 html 的 font-size=16px,则 1rem=16px,元素设置 width: 10rem,实际宽度为 160px;
- 动态适配不同屏幕:通过 JS 动态计算并设置 html 的 font-size,使其与屏幕宽度成固定比例。例如:设计稿宽度为 375px,设置 1rem=100px(即 html 的 font-size=100px),则设计稿中 375px 的宽度对应 3.75rem;在屏幕宽度为 750px 的设备上,动态设置 html 的 font-size=200px,3.75rem 对应的实际宽度为 750px,实现等比例缩放;
- 结合 viewport 元标签:移动端需设置 viewport 元标签,禁止页面缩放,确保屏幕宽度为设备物理宽度。示例:;
- 实现步骤示例:
// 设计稿宽度375px,设置1rem=100px(即设计稿1px=1/100 rem) function setRem() { const designWidth = 375; const rootFontSize = 100; // 计算当前屏幕宽度与设计稿宽度的比例 const scale = window.innerWidth / designWidth; // 动态设置html的font-size(限制最小和最大比例,避免极端情况) const fontSize = Math.min(Math.max(rootFontSize * scale, 80), 120); document.documentElement.style.fontSize = fontSize + 'px'; } // 页面加载和窗口 resize 时执行 window.addEventListener('load', setRem); window.addEventListener('resize', setRem);
优点:实现简单,适配所有移动端屏幕;缺点:需动态设置根元素字体大小,且 rem 计算需转换设计稿尺寸(可通过构建工具自动转换)。
37. vue 中的 watch 方法第一次页面加载时执行吗?如果需要执行怎么实现?
一、默认情况
Vue 中的 watch 方法默认在第一次页面加载时不执行,仅在监听的属性发生变化时才会触发回调函数。
原因:watch 的核心作用是“监听属性变化并执行回调”,默认设计为“变化时触发”,初始加载时属性未发生变化,因此不执行。
二、需要第一次加载时执行的实现方法
通过 watch 的 immediate 选项实现,设置 immediate: true,即可让 watch 在页面初始加载时执行一次回调函数。
1. Vue2 实现示例:
new Vue({
data() {
return {
name: '张三' // 初始值
};
},
watch: {
name: {
// 回调函数,val为新值,oldVal为旧值(初始加载时oldVal为undefined)
handler(val, oldVal) {
console.log('name值变化/初始加载', val, oldVal);
},
immediate: true // 开启初始加载执行
}
}
});
2. Vue3 选项式 API 实现:
export default {
data() {
return { name: '张三' };
},
watch: {
name: {
handler(val) {
console.log('初始加载/变化时执行', val);
},
immediate: true
}
}
};
3. Vue3 组合式 API(watch 函数)实现:
import { ref, watch } from 'vue';
export default {
setup() {
const name = ref('张三');
// 第三个参数传入{ immediate: true }
watch(
name,
(val, oldVal) => {
console.log('初始加载/变化时执行', val, oldVal);
},
{ immediate: true }
);
return { name };
}
};
三、补充说明
- immediate: true 时,回调函数的 oldVal 为 undefined(初始加载时无旧值);
- 若需监听对象的深层属性,需同时设置 deep: true(与 immediate 可同时使用)。示例:
watch: { 'user.info': { handler(val) { console.log(val); }, immediate: true, deep: true // 深层监听 } }
38. 两个数组怎么合并?输出两种方法。两个对象怎么合并?
一、两个数组合并(两种方法)
- **扩展运算符(...)**: 原理:将两个数组展开为独立元素,再用新数组包裹,生成新数组(不改变原数组)。示例:
const arr1 = [1, 2, 3]; const arr2 = [4, 5, 6]; const newArr = [...arr1, ...arr2]; // 结果:[1,2,3,4,5,6] console.log(arr1, arr2); // 原数组不变:[1,2,3]、[4,5,6]优点:语法简洁,不改变原数组;缺点:若数组元素为引用类型,仅浅拷贝。 - concat()方法: 原理:concat()方法用于连接两个或多个数组,返回新数组(不改变原数组)。示例:
const arr1 = [1, 2, 3]; const arr2 = [4, 5, 6]; const newArr = arr1.concat(arr2); // 结果:[1,2,3,4,5,6] // 也可连接多个数组:arr1.concat(arr2, arr3, arr4)优点:兼容性好(支持 ES5 及以下),不改变原数组;缺点:同样是浅拷贝。
补充:若需深合并(数组元素为引用类型),需结合深拷贝方法(如 deepClone 函数)。
二、两个对象合并
常用 3 种方法,核心是“合并对象属性,后一个对象属性覆盖前一个对象同名属性”:
- **扩展运算符(...)**:
const obj1 = { name: '张三', age: 25 }; const obj2 = { age: 30, gender: '男' }; const newObj = { ...obj1, ...obj2 }; // 结果:{ name: '张三', age: 30, gender: '男' }优点:语法简洁,不改变原对象;缺点:浅拷贝,引用类型属性会共享。 - **Objec