引言
时隔半个月来面试,这次面试官语气听上去有点严肃,再加上好久没有面试,使得我这次面试也有点紧张,面试官问了差不多有20道题目,但很多都是顺着我的回答往下问的。这次面试官的问题总体来说不难,但他问的JS有的比较深入,我甚至都没有接触过(比如Object.freeze()浅冻结等等),所以我写下这篇面试经验总结,希望能对你有份帮助
1. const let var 区别,const定义对象可以新增属性吗?如何让const定义的属性不被修改?
三者区别:
- 作用域:var具有函数作用域,let和const具有块级作用域
- 变量提升:var会提升到作用域顶部,let和const不会
- 重复声明:var允许重复声明,let和const不允许
- 初始化:var可以先使用后声明,let和const必须先声明后使用(存在暂时性死区)
- 可变性:var和let声明的变量可以重新赋值,const声明的变量不能重新赋值
const定义对象:
- const定义的对象本身不能重新赋值,但可以修改或新增其属性
- 这是因为const只保证引用地址不变,而不保证对象内容不变
让const定义的属性不被修改:
- 使用
Object.freeze()方法进行浅冻结,阻止对象属性的添加、删除和修改 - 但这只是浅冻结,如果对象包含嵌套对象,嵌套对象的属性仍可修改
- 对于深冻结,需要递归调用
Object.freeze()冻结所有层级的对象
2. 如何深拷贝一个对象?如果JSON.parse(JSON.stringify(undefined))会发生什么?
深拷贝方法:
- JSON.parse(JSON.stringify(obj)):
- 简单有效,但无法处理函数、undefined、循环引用、Date对象、正则表达式等
- 递归实现深拷贝:
function deepClone(obj, map = new Map()) { if (obj === null || typeof obj !== 'object') return obj; if (obj instanceof Date) return new Date(obj); if (obj instanceof RegExp) return new RegExp(obj); if (map.has(obj)) return map.get(obj); // 处理循环引用 let cloneObj = Array.isArray(obj) ? [] : {}; map.set(obj, cloneObj); for (let key in obj) { if (obj.hasOwnProperty(key)) { cloneObj[key] = deepClone(obj[key], map); } } return cloneObj; } - 使用第三方库:如lodash的
_.cloneDeep()方法
JSON.parse(JSON.stringify(undefined)):会报错,因为JSON.stringify(undefined)返回undefined,而JSON.parse()需要接收一个有效的JSON字符串
3. 什么是闭包?
**闭包定义:**闭包是指有权访问另一个函数作用域中变量的函数。
形成条件:
- 函数嵌套
- 内部函数引用了外部函数的变量
- 外部函数被调用,内部函数被返回并在外部被引用
作用:
- 保护变量不被全局污染
- 实现变量私有化
- 维持变量的持久化(变量不会在函数执行后被垃圾回收)
示例:
function outer() {
let count = 0;
return function inner() {
count++;
return count;
};
}
const counter = outer();
console.log(counter()); // 1
console.log(counter()); // 2
4. 你知道数组方法有哪些?forEach和map区别?基本数据类型,引用数据类型forEach都会修改吗?如果只是定义了一个简单的数组[1,2,3,4]都加1会改变吗?
常用数组方法:
- 遍历/转换:forEach、map、filter、reduce、find、findIndex、some、every
- 修改数组:push、pop、shift、unshift、splice、sort、reverse
- 其他:concat、slice、join、includes、indexOf、lastIndexOf等
forEach和map区别:
- 返回值:map返回新数组,forEach没有返回值(返回undefined)
- 用途:map用于数据转换,forEach仅用于遍历
- 可链式调用:map可以链式调用其他数组方法,forEach不能
forEach对数据类型的修改:
- 对于基本数据类型(数字、字符串、布尔值等):在forEach中修改元素值不会改变原数组
- 对于引用数据类型(对象、数组等):在forEach中修改元素的属性会改变原数组
- 简单数组
[1,2,3,4]在forEach中加1:如果直接修改元素值,原数组不会改变;如果使用索引修改,则会改变
5. 介绍reduce方法,介绍里面的参数,参数的作用是什么?
**reduce方法:**用于对数组中的所有元素执行一个累积操作,将其减少为单个值。
语法:arr.reduce(callback(accumulator, currentValue, currentIndex, array), initialValue)
参数说明:
- callback:执行每个元素的函数,包含四个参数:
accumulator:累积器,存储上一次回调函数的返回值currentValue:当前正在处理的元素currentIndex(可选):当前元素的索引array(可选):调用reduce的原数组
- initialValue(可选):累积器的初始值
作用:
- 数组求和、求乘积
- 数组转对象
- 数组扁平化
- 计算数组中元素出现次数
- 实现数组去重
示例:
// 数组求和
const sum = [1, 2, 3, 4].reduce((acc, curr) => acc + curr, 0); // 10
6. 使用数组会进行链式调用,自己封装链式调用应该怎么做?如何实现?
链式调用实现思路: 通过类或构造函数封装数组操作,每个方法执行后返回this(实例本身),从而实现链式调用。
实现示例:
class ChainArray {
constructor(arr) {
this.arr = arr;
}
map(callback) {
this.arr = this.arr.map(callback);
return this; // 返回this实现链式调用
}
filter(callback) {
this.arr = this.arr.filter(callback);
return this;
}
forEach(callback) {
this.arr.forEach(callback);
return this;
}
// 获取最终结果
value() {
return this.arr;
}
}
// 使用示例
const result = new ChainArray([1, 2, 3, 4])
.map(x => x * 2)
.filter(x => x > 4)
.value();
console.log(result); // [6, 8]
7. 说一下你了解的promise?promise.all一个失败会怎么样?上传10张图片,3张失败,如何知道哪些成功哪些失败?
**Promise定义:**Promise是一种用于处理异步操作的对象,表示一个异步操作的最终完成(或失败)及其结果值。
Promise状态:
- pending:初始状态
- fulfilled:操作成功完成
- rejected:操作失败
Promise方法:
- Promise.resolve():返回一个已解决的Promise
- Promise.reject():返回一个已拒绝的Promise
- Promise.all():接收Promise数组,全部成功才成功,任何一个失败就失败
- Promise.race():接收Promise数组,第一个完成的结果决定最终状态
- Promise.allSettled():接收Promise数组,等待所有Promise完成(无论成功失败),返回所有结果
- Promise.any():接收Promise数组,任何一个成功就成功,全部失败才失败
Promise.all失败情况:
- 当Promise.all中的任何一个Promise被拒绝,整个Promise.all就会立即被拒绝,返回第一个被拒绝的原因
- 不会等待其他Promise完成
处理部分失败的图片上传:
使用Promise.allSettled()方法,它会等待所有Promise都完成(无论成功或失败),然后返回一个包含每个Promise结果的数组,可以通过检查每个结果的status来确定哪些成功哪些失败。
8. ts用过不?
(根据实际情况回答,这里提供示例答案) 是的,我在项目中使用过TypeScript。TypeScript是JavaScript的超集,添加了静态类型系统和一些ES6+的特性,帮助开发者在开发阶段发现类型错误,提高代码质量和可维护性。我主要使用TypeScript进行React项目开发,包括定义接口、类型注解、泛型等功能。
9. ts 里面的any,unknown,never有什么区别?
any:
- 表示任意类型,完全放弃类型检查
- 可以赋值给任何类型,任何类型也可以赋值给any
- 不建议过度使用,会失去TypeScript的类型安全优势
unknown:
- 表示未知类型,是类型安全的any
- 可以赋值给unknown类型,但unknown类型不能赋值给其他类型(除非进行类型断言或类型守卫)
- 适合处理类型不确定的情况,比any更安全
never:
- 表示永远不会发生的值的类型
- 用于函数永远不会返回(如抛出异常或无限循环)的返回类型
- 是所有类型的子类型,可以赋值给任何类型
10. interface type 区别?
相同点:
- 都可以用来定义对象或函数的类型
- 都可以使用泛型
- 都可以扩展(interface使用extends,type使用交叉类型&)
不同点:
- 声明合并:interface可以多次声明并自动合并,type不可以
- 联合类型:type可以定义联合类型(如
type A = string | number),interface不行 - 类型别名:type可以为任何类型创建别名(如基本类型、联合类型等),interface只能定义对象或函数类型
- 映射类型:type可以配合in关键字创建映射类型,interface不行
- 使用场景:接口更适合定义对象的形状,类型别名更适合创建复杂类型
11. ts的内置高级类型(pick,omit 有用过吗?)
常用内置高级类型:
- Pick<T, K>:从类型T中选取指定的属性K,创建新类型
interface User { id: number; name: string; age: number; } type UserName = Pick<User, 'name'>; // { name: string } - Omit<T, K>:从类型T中排除指定的属性K,创建新类型
type UserWithoutAge = Omit<User, 'age'>; // { id: number; name: string } - Partial:将类型T的所有属性变为可选
- Required:将类型T的所有属性变为必需
- Readonly:将类型T的所有属性变为只读
- Record<K, T>:创建一个键为K类型、值为T类型的对象类型
- Exclude<T, U>:从类型T中排除可以赋值给U的类型
- Extract<T, U>:从类型T中提取可以赋值给U的类型
- ReturnType:获取函数T的返回值类型
12. 你知道flex属性有哪些?flex:1是哪几个属性的结合?
flex属性组成: flex属性是flex-grow、flex-shrink和flex-basis三个属性的简写。
- flex-grow:定义项目的放大比例,默认为0
- flex-shrink:定义项目的缩小比例,默认为1
- flex-basis:定义项目在分配多余空间之前的默认大小,默认为auto
flex:1的构成:
flex:1等价于flex:1 1 0%,表示:
flex-grow:1:项目可以放大,占据剩余空间flex-shrink:1:项目可以缩小,当空间不足时参与收缩flex-basis:0%:项目的基准大小为0,分配空间时不考虑自身内容
13. 页面有6个元素,上下各3个,无论屏幕多宽,它会随着屏幕去自适应,不会去换行(grid布局)
使用Grid布局可以轻松实现这种需求:
.container {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 3列,每列等宽 */
grid-template-rows: repeat(2, 1fr); /* 2行,每行等高 */
gap: 10px; /* 元素间距 */
width: 100%; /* 容器宽度100% */
}
.item {
background-color: #f0f0f0;
padding: 20px;
text-align: center;
}
这种布局的特点是:
- 无论屏幕宽度如何变化,始终保持3列2行的布局
- 每列宽度会自动适应容器宽度(1fr表示等分剩余空间)
- 元素不会换行,始终保持网格布局
14. BFC是什么?
**BFC定义:**BFC(Block Formatting Context,块级格式化上下文)是Web页面中盒模型布局的一种CSS渲染模式,是一个独立的渲染区域,规定了内部的块级元素如何布局。
触发BFC的条件:
- 根元素(html)
- float的值不为none
- position的值为absolute或fixed
- display的值为inline-block、table-cell、table-caption、flex、inline-flex
- overflow的值不为visible
BFC的特性:
- 内部的块级元素会在垂直方向上一个接一个地放置
- 元素的左外边距与包含块的左边界相接触(从左到右的格式化上下文)
- 同一BFC内的相邻块级元素的外边距会发生折叠
- BFC的区域不会与float元素重叠
- BFC是一个隔离的独立容器,内部元素的布局不会影响外部元素
- 计算BFC的高度时,浮动元素也会参与计算
BFC的应用:
- 解决外边距折叠问题
- 清除浮动
- 防止元素被浮动元素覆盖
- 布局(如两栏布局)
15. useState,useRef有什么区别?
useState:
- 用于管理组件的状态
- 状态更新会触发组件重新渲染
- 返回一个包含当前状态和更新函数的数组
[state, setState] - 状态更新是异步的
- 可以使用函数式更新
setState(prevState => prevState + 1) - 常用于需要根据状态变化重新渲染UI的场景
useRef:
- 用于创建一个可变的ref对象,其.current属性可以存储任意值
- 改变ref的值不会触发组件重新渲染
- 返回一个带有current属性的对象
{ current: initialValue } - 引用值的更新是同步的
- 常用于访问DOM元素、存储不需要引起重渲染的值、保存上一次渲染的状态
主要区别:
- useState管理的状态变化会触发组件重渲染,useRef不会
- useState返回数组,useRef返回对象
- useState用于需要响应式更新的状态,useRef用于非响应式数据或DOM引用
16. 聊聊useMemo和useCallback
useMemo:
useMemo(() => computedValue, [dependencies])- 用于缓存计算结果,避免在每次渲染时都重新计算
- 接收一个计算函数和依赖数组,只有当依赖项变化时才会重新计算
- 返回计算结果
- 常用于优化计算开销大的操作
useCallback:
useCallback(() => { /* 函数体 */ }, [dependencies])- 用于缓存函数引用,避免在每次渲染时都创建新的函数实例
- 接收一个函数和依赖数组,只有当依赖项变化时才会返回新的函数实例
- 返回缓存的函数
- 常用于优化子组件的重渲染(当函数作为props传递给子组件时)
共同点:
- 都是React的性能优化Hook
- 都依赖于依赖数组来决定是否重新计算/创建
- 都遵循闭包规则,只能访问定义时的变量
使用场景:
- useMemo:缓存计算结果(如复杂的数学计算、过滤大型数组等)
- useCallback:缓存事件处理函数、回调函数等
17. useContext react状态管理用的什么?
useContext:
- useContext是React提供的一个Hook,用于访问React Context中的数据
- Context提供了一种在组件之间共享值的方式,而不必显式地通过组件树的逐层传递props
使用步骤:
- 创建Context:
const MyContext = React.createContext(defaultValue) - 提供Context值:使用
MyContext.Provider组件包裹需要访问Context的组件树,并通过value属性传递数据 - 消费Context值:在子组件中使用
useContext(MyContext)获取Context的值
React状态管理方案: 除了useContext,React常用的状态管理方案还有:
- 内置Hook:useState(组件级状态)、useReducer(复杂状态逻辑)
- Context API + useReducer:适合中小型应用的全局状态管理
- 第三方库:Redux、MobX、Zustand、Jotai等,适合大型应用的复杂状态管理
18. 介绍下封装过的hooks
(根据实际情况回答,这里提供示例答案)
在项目中,我封装过以下一些常用的自定义Hooks:
- useFetch:封装数据获取逻辑
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error('Network response was not ok');
const result = await response.json();
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
}, [url, JSON.stringify(options)]);
return { data, loading, error };
}
- useLocalStorage:封装localStorage操作
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
- useDebounce:封装防抖逻辑
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
19. webWork了解过吗?登录有20个接口做缓存,这些数据不经常被修改,一个表单有几十个下拉,不可能创建详情页去请求这20个下拉,20个缓存接口做到登录时候,怎么优化?如何登录做缓存?做表单保存做过管理系统吗?
Web Worker:
- Web Worker是HTML5标准的一部分,允许在后台线程中运行JavaScript代码,而不阻塞主线程
- 主要用于处理计算密集型或高延迟的任务,如大量数据处理、复杂计算等
- Worker线程与主线程之间通过postMessage()方法和onmessage事件进行通信
- Worker无法直接访问DOM、window对象和document对象
登录接口缓存优化方案:
- 集中管理缓存:创建一个缓存管理服务,统一处理20个接口的数据缓存
- 并行请求:登录成功后,使用Promise.all并行请求这20个接口,减少总加载时间
- 本地存储:将获取到的数据存储在localStorage或sessionStorage中
- 缓存策略:
- 设置缓存过期时间
- 添加版本控制,当数据结构变更时可以强制刷新缓存
- 提供手动刷新缓存的机制
- 按需加载:对于不是立即可用的数据,可以考虑按需加载,先显示页面框架,数据加载完成后再更新
- 状态管理:使用Redux或Context API等状态管理工具,将缓存数据存储在全局状态中,方便表单组件访问
表单保存(管理系统经验): (根据实际情况回答,这里提供示例答案) 是的,我在管理系统开发中实现过表单保存功能。主要包括:
- 自动保存:用户输入时定时自动保存到临时存储
- 草稿功能:允许用户保存草稿,稍后继续编辑
- 提交验证:提交前进行表单验证,确保数据完整性
- 提交反馈:显示提交状态(成功/失败)和错误信息
- 历史记录:保存表单提交的历史版本,支持查看和回滚
20. 有了解过微前端吗?
**微前端定义:**微前端是一种架构模式,将前端应用分解为更小、更简单的独立部署单元(微应用),每个微应用可以由不同的团队开发,使用不同的技术栈,但在用户界面上仍然是一个统一的整体。
微前端的核心思想:
- 独立开发:每个微应用可以独立开发、测试和部署
- 独立运行:每个微应用可以独立运行,不依赖其他微应用
- 无缝集成:在用户界面上,多个微应用无缝集成,看起来像一个单一应用
- 技术无关:不同的微应用可以使用不同的技术栈
常见微前端框架:
- Single-SPA:最早的微前端框架,支持多种框架共存
- Qiankun:基于Single-SPA,提供了更完善的开箱即用功能
- MicroApp:京东开源的微前端框架,基于Web Components
- EMP:基于Webpack5 Module Federation的微前端解决方案
微前端的挑战:
- 样式隔离:防止不同微应用之间的CSS冲突
- 状态管理:跨微应用的状态共享和通信
- 路由管理:确保不同微应用之间的路由切换平滑
- 性能优化:避免重复加载公共依赖
- 部署协调:多个微应用的版本管理和部署协调
实现方式:
- 基于路由:通过路由分发,不同的路由加载不同的微应用
- 基于组件:将微应用封装为组件,在主应用中直接使用
- 基于iframe:使用iframe加载微应用,隔离性最好但性能和体验较差
- 基于Web Components:将微应用封装为自定义元素,实现组件级别的复用