美团一二面东方财富一面
1. 请尽可能详细地说明,为什么React hooks不能写在条件和循环表达式中?你的回答中不要写出示例代码。
React Hooks 不能写在条件和循环表达式中的原因主要涉及到 React 的内部工作机制和 Hooks 的设计原则。以下是详细的解释:
1. Hooks 的调用顺序
React Hooks 必须在函数组件的顶层调用,不能嵌套在条件语句或循环语句中。这是因为 React 依赖于 Hooks 的调用顺序来管理组件的状态和副作用。
-
状态管理:
- React 使用一个数组来存储每个组件的 Hooks 状态。每次组件渲染时,React 都会按照相同的顺序调用 Hooks,并更新相应的状态。
- 如果 Hooks 被写在条件语句或循环语句中,调用顺序可能会发生变化,导致状态管理混乱。
-
副作用管理:
- 类似的,React 的
useEffect
和useLayoutEffect
Hooks 也依赖于固定的调用顺序来管理副作用。 - 如果这些 Hooks 被写在条件语句或循环语句中,副作用的执行顺序可能会被打乱,导致不可预期的行为。
- 类似的,React 的
2. 组件的确定性
React 组件的行为应该是确定性的,即相同的输入应该产生相同的结果。Hooks 的调用顺序保证了这一点。
- 一致性:
- 如果 Hooks 可以在条件语句或循环语句中调用,组件的行为将变得不确定,因为每次渲染时 Hooks 的调用顺序可能不同。
- 这会使得组件的状态和副作用难以预测和维护。
3. 避免副作用的意外触发
Hooks 如 useEffect
和 useLayoutEffect
用于处理副作用,如数据获取、订阅和手动 DOM 操作。如果这些 Hooks 被写在条件语句或循环语句中,可能会导致副作用的意外触发。
- 副作用的控制:
- 副作用应该在组件的每次渲染时都以相同的方式执行,以确保行为的一致性。
- 如果副作用依赖于某些条件,应该将这些条件放在 Hooks 内部的逻辑中,而不是在调用 Hooks 时进行条件判断。
4. 简化 Hooks 的使用
Hooks 的设计目标是简化函数组件的开发,使其能够处理状态和副作用,而不需要类组件的复杂性。如果 Hooks 可以在条件语句或循环语句中调用,将会增加使用的复杂性和潜在的错误。
- 易用性:
- 将 Hooks 限制在函数组件的顶层调用,使得 Hooks 的使用更加直观和简单。
- 开发者可以更容易地理解和维护组件的状态和副作用逻辑。
总结
React Hooks 不能写在条件和循环表达式中,主要是为了保证 Hooks 的调用顺序一致,确保组件的行为确定性和副作用的可控性。这一设计原则简化了 Hooks 的使用,提升了组件的可维护性和可靠性。
2. 请尽可能详细地说明,哪些应用场景下应该使用Webpack,哪些应该使用Vite?你的回答中不要写出示例代码。
Webpack 和 Vite 都是现代前端开发中常用的构建工具,但它们在不同的应用场景下有不同的优势和适用性。以下是详细的应用场景分析:
使用 Webpack 的场景
-
大型复杂项目:
- Webpack 的强大之处在于其丰富的插件生态系统和高度可配置的特性,使其非常适合大型、复杂的前端项目。
- Webpack 的代码分割、懒加载和 Tree Shaking 功能能够有效优化大型应用的性能。
-
多模块项目:
- Webpack 支持多种模块系统(如 CommonJS、AMD、ES6 Modules),适合需要处理多种模块格式的项目。
- Webpack 的依赖管理和模块解析能力使其在多模块项目中表现出色。
-
传统开发和生产环境:
- Webpack 在传统的前端开发流程中已经被广泛使用,许多团队已经熟悉其配置和使用方法。
- Webpack 提供了完善的开发和生产环境配置,适合需要精细控制的场景。
-
需要复杂构建逻辑:
- Webpack 的插件系统非常强大,可以实现复杂的构建逻辑,如自定义资源处理、环境变量注入等。
- 适合需要进行大量自定义构建步骤的项目。
-
兼容旧版浏览器:
- Webpack 可以通过 Babel 和 PostCSS 等工具链很好地支持旧版浏览器的兼容性处理。
- 适合需要兼容旧版浏览器的项目。
使用 Vite 的场景
-
现代 JavaScript 项目:
- Vite 利用了现代浏览器对 ES 模块的原生支持,适合使用最新 JavaScript 特性的项目。
- Vite 的快速冷启动和模块热更新使其在开发过程中表现出色。
-
中小型项目:
- Vite 的简洁配置和快速构建速度使其非常适合中小型项目,能够显著提升开发效率。
- Vite 的开箱即用特性减少了配置的复杂性,适合快速开发和迭代。
-
现代前端框架:
- Vite 对 Vue 3 和 React 等现代前端框架有很好的支持,特别是 Vue 3 的官方推荐构建工具。
- 适合使用这些框架进行开发的项目。
-
追求极致开发体验:
- Vite 的模块热更新(HMR)速度非常快,几乎可以实现即时更新,适合追求极致开发体验的团队。
- Vite 的开发服务器和构建工具一体化设计,简化了开发流程。
-
需要快速原型开发:
- Vite 的快速启动和热更新特性使其非常适合快速原型开发和迭代。
- 适合需要频繁修改和验证想法的开发阶段。
总结
- Webpack 更适合大型复杂项目、多模块项目、传统开发和生产环境、需要复杂构建逻辑以及兼容旧版浏览器的场景。
- Vite 更适合现代 JavaScript 项目、中小型项目、现代前端框架、追求极致开发体验以及需要快速原型开发的场景。
选择 Webpack 还是 Vite 应根据项目的具体需求、团队的技术栈和偏好来决定。两者各有优势,合理选择可以显著提升开发效率和项目性能。
补充:请尽可能详细地说明,哪些情况下建议从Webpack迁移到vite?你的回答中不要写出示例代码。
从 Webpack 迁移到 Vite 的决策通常基于以下几个方面的考虑:
1. 项目类型和规模
-
现代 JavaScript 项目:
- Vite 利用了现代浏览器对 ES 模块的原生支持,适合使用最新 JavaScript 特性的项目。
- 如果项目主要使用 ES6+ 模块和现代前端框架(如 Vue 3、React),Vite 可以提供更好的开发体验和性能。
-
中小型项目:
- Vite 的简洁配置和快速构建速度使其非常适合中小型项目,能够显著提升开发效率。
- 对于不需要复杂配置和插件生态系统的项目,Vite 是一个更轻量级的选择。
2. 开发体验
-
快速的冷启动和热更新:
- Vite 的冷启动速度非常快,几乎可以实现即时启动。
- Vite 的模块热更新(HMR)速度也非常快,几乎可以实现即时更新,适合追求极致开发体验的团队。
-
简洁的配置:
- Vite 的配置文件非常简洁,默认配置已经涵盖了大部分常见需求。
- 如果团队希望减少配置的复杂性,Vite 是一个更好的选择。
3. 性能优化
-
按需编译:
- Vite 采用按需编译的方式,只编译当前使用的模块,而不是整个应用,这在大型项目中可以显著提升开发服务器的启动速度。
- 这种按需编译的方式也使得 Vite 在开发过程中能够更快地响应文件变化。
-
优化的生产构建:
- Vite 使用 Rollup 进行生产构建,Rollup 在处理 ES 模块时具有天然的优势,能够生成更小、更快的代码包。
- 如果项目对生产环境的构建性能和输出质量有较高要求,Vite 是一个更好的选择。
4. 生态系统的支持
-
现代前端框架的支持:
- Vite 对 Vue 3 和 React 等现代前端框架有很好的支持,特别是 Vue 3 的官方推荐构建工具。
- 如果项目使用这些框架,迁移到 Vite 可以获得更好的支持和优化。
-
插件生态系统的成熟度:
- 虽然 Vite 的插件生态系统还在快速发展中,但已经有很多高质量的插件可供选择。
- 如果项目依赖的插件在 Vite 生态系统中有良好的支持,迁移到 Vite 可以保持开发流程的一致性。
5. 长期维护和未来规划
-
技术栈的现代化:
- Vite 是一个相对较新的工具,代表了前端构建工具的未来发展方向。
- 如果团队希望保持技术栈的现代化,迁移到 Vite 可以更好地适应未来的技术趋势。
-
团队的技术偏好:
- 如果团队成员对 Vite 更加熟悉和偏好,迁移到 Vite 可以提高团队的整体开发效率和满意度。
总结
从 Webpack 迁移到 Vite 的决策应综合考虑项目类型和规模、开发体验、性能优化、生态系统的支持以及长期维护和未来规划等因素。对于现代 JavaScript 项目、中小型项目、追求极致开发体验的项目以及对性能有较高要求的项目,迁移到 Vite 可以带来显著的优势。
3. 请尽可能详细地说明,React super() 和 super(props) 有什么区别?你的回答中不要写出示例代码。
在 React 中,super()
和 super(props)
主要用于在类组件的构造函数中调用父类的构造函数。以下是它们的区别和使用场景:
super()
-
基本用法:
super()
是 JavaScript 中用于调用父类构造函数的语法。- 在 React 类组件的构造函数中使用
super()
,可以调用 React.Component 的构造函数。
-
不传递参数:
super()
不传递任何参数给父类的构造函数。- 这意味着在调用
super()
后,this.props
是未定义的,无法在构造函数中直接访问this.props
。
-
适用场景:
- 如果构造函数中不需要访问
this.props
,可以使用super()
。 - 这种情况通常出现在构造函数中只进行一些简单的初始化操作,而不需要依赖外部传入的 props。
- 如果构造函数中不需要访问
super(props)
-
传递参数:
super(props)
在调用父类构造函数时传递了props
参数。- 这使得在构造函数中可以直接访问
this.props
。
-
适用场景:
- 如果构造函数中需要访问
this.props
,必须使用super(props)
。 - 这种情况通常出现在构造函数中需要根据传入的 props 进行一些初始化操作,如设置初始状态。
- 如果构造函数中需要访问
具体区别
-
this.props
的可用性:- 使用
super()
时,this.props
在构造函数中是未定义的。 - 使用
super(props)
时,this.props
在构造函数中是可用的。
- 使用
-
最佳实践:
- 在大多数情况下,推荐在构造函数中使用
super(props)
,以确保this.props
在整个构造函数中都是可用的。 - 只有在确定构造函数中不需要访问
this.props
时,才可以选择使用super()
。
- 在大多数情况下,推荐在构造函数中使用
总结
- 使用
super()
时,构造函数中无法访问this.props
。 - 使用
super(props)
时,构造函数中可以直接访问this.props
。 - 为了确保代码的健壮性和可维护性,推荐在构造函数中始终使用
super(props)
。
4. (美团一)算法:闭包
function func(){
var i=0;
/*
*你的代码
*/
}
var funcA = func()
funcA() //1
funcA() //2
funcA() //3
function func(){
var i=0;
return ()=>{
console.log(++i);
}
}
var funcA = func()
funcA() //1
funcA() //2
funcA() //3
5. (美团一)算法:矩阵转置
const matrix = [
[1,2,3],
[4,5,6],
[7,8,9]
]
实现matrixTrans(matrix)
输出
[
[1,4,7],
[2,5,8],
[3,6,9],
]
const matrixTrans=(matrix:ReadonlyArray<ReadonlyArray<number>>)=>{
const newMatrix:number[][]=[];
for(let i=0;i<matrix.length;i++){
const thisLine=matrix[i]; // 多余
for(let j=0;j<thisLine.length;j++){
if(!newMatrix[j]){
newMatrix[j]=[];
}
newMatrix[j][i]=matrix[i][j];
}
}
return newMatrix;
}
console.log(JSON.stringify(matrixTrans([
[1,2,3],
[4,5,6],
[7,8,9]
])))
6. (东方财富一)算法:手写Promise
请尽可能详细地用TypeScript实现一个Promise类。
在 TypeScript 中实现一个 Promise
类是一项复杂的任务,因为它需要模拟原生 Promise
的行为,包括状态管理、异步操作和错误处理。以下是一个简化的 Promise
类实现,展示了核心概念和功能:
type Executor<T> = (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void;
enum PromiseState {
PENDING,
FULFILLED,
REJECTED
}
class MyPromise<T> {
private state: PromiseState = PromiseState.PENDING;
private value: T | undefined;
private reason: any;
private onFulfilledCallbacks: Array<(value: T) => void> = [];
private onRejectedCallbacks: Array<(reason: any) => void> = [];
constructor(executor: Executor<T>) {
const resolve = (value: T | PromiseLike<T>): void => {
if (this.state === PromiseState.PENDING) {
if (value instanceof MyPromise) {
value.then(resolve, reject);
} else {
this.state = PromiseState.FULFILLED;
this.value = value;
this.onFulfilledCallbacks.forEach(callback => callback(value));
}
}
};
const reject = (reason: any): void => {
if (this.state === PromiseState.PENDING) {
this.state = PromiseState.REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(callback => callback(reason));
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
): MyPromise<TResult1 | TResult2> {
return new MyPromise<TResult1 | TResult2>((resolve, reject) => {
const fulfilledHandler = (value: T): void => {
try {
if (onFulfilled) {
resolve(onFulfilled(value));
} else {
resolve(value as unknown as TResult1);
}
} catch (error) {
reject(error);
}
};
const rejectedHandler = (reason: any): void => {
try {
if (onRejected) {
resolve(onRejected(reason));
} else {
reject(reason);
}
} catch (error) {
reject(error);
}
};
if (this.state === PromiseState.FULFILLED) {
setTimeout(() => fulfilledHandler(this.value!), 0);
} else if (this.state === PromiseState.REJECTED) {
setTimeout(() => rejectedHandler(this.reason), 0);
} else {
this.onFulfilledCallbacks.push(fulfilledHandler);
this.onRejectedCallbacks.push(rejectedHandler);
}
});
}
catch<TResult = never>(onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): MyPromise<T | TResult> {
return this.then(undefined, onRejected);
}
static resolve<T>(value: T | PromiseLike<T>): MyPromise<T> {
return new MyPromise<T>(resolve => resolve(value));
}
static reject<T = never>(reason?: any): MyPromise<T> {
return new MyPromise<T>((_, reject) => reject(reason));
}
static all<T>(promises: Array<MyPromise<T>>): MyPromise<Array<T>> {
return new MyPromise<Array<T>>((resolve, reject) => {
const results: Array<T> = [];
let completedCount = 0;
promises.forEach((promise, index) => {
promise.then(
value => {
results[index] = value;
completedCount++;
if (completedCount === promises.length) {
resolve(results);
}
},
reason => {
reject(reason);
}
);
});
});
}
static race<T>(promises: Array<MyPromise<T>>): MyPromise<T> {
return new MyPromise<T>((resolve, reject) => {
promises.forEach(promise => {
promise.then(
value => resolve(value),
reason => reject(reason)
);
});
});
}
}
解释
-
类型定义:
Executor<T>
是一个函数类型,接受两个参数:resolve
和reject
。
-
状态管理:
- 使用
PromiseState
枚举来表示PENDING
、FULFILLED
和REJECTED
三种状态。 state
属性用于跟踪当前的 Promise 状态。value
和reason
属性分别用于存储成功和失败的结果。
- 使用
-
回调函数管理:
onFulfilledCallbacks
和onRejectedCallbacks
数组用于存储成功和失败的回调函数。
-
构造函数:
- 接受一个
executor
函数,并在其中定义resolve
和reject
函数。 - 调用
executor
函数,并捕获可能的错误。
- 接受一个
-
then 方法:
- 返回一个新的
MyPromise
实例。 - 根据当前状态,立即调用回调函数或将其存储起来等待调用。
- 返回一个新的
-
catch 方法:
- 调用
then
方法,只传递onRejected
回调函数。
- 调用
-
静态方法:
resolve
和reject
方法用于创建已解决或已拒绝的 Promise。all
方法接受一个 Promise 数组,并在所有 Promise 都成功时返回结果数组,或在任意一个 Promise 失败时返回失败原因。race
方法接受一个 Promise 数组,并返回最先解决或拒绝的 Promise 的结果。
这个实现展示了 Promise
的核心功能和基本用法,但并不完全等同于原生的 Promise
,因为它省略了一些边缘情况和优化。
7. (美团二)算法:二叉树中的最大路径和
在二叉树中,路径被定义为一组节点的序列,每对相邻节点之间存在一条边。同一个节点只能在路径序列中出现一次。路径必须包含至少一个节点,且不一定经过根节点。
路径和指的是路径中所有节点值的总和。
给定一个二叉树的根节点root,求其最大路径和。
备注
树中节点数目范围是 [1, 3 * 104]
-1000 <= Node.val <= 1000
示例1
输入
root = [1,-2,3,4]
输出
6
解释
最优路径是 4 -> -2 -> 1 -> 3 ,路径和为 4 - 2 + 1 + 3 = 6
示例2
输入
root = [1,-2,3,4,5,6,7]
输出
16
解释
最优路径是 6 -> 3 -> 7 ,路径和为 6 + 3 + 7 = 16
LeetCode hot 150 ——hard
LeetCode 124
https://leetcode.cn/problems/binary-tree-maximum-path-sum/description/?envType=problem-list-v2&envId=binary-tree
// 代码中已指定的类名、方法名、参数名,请勿修改,直接返回方法规定的值即可
/**
* Definition for a binary tree node.
* class TreeNode {
* val: number
* left: TreeNode | null
* right: TreeNode | null
* constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
* }
**/
class TreeNode {
val: number
left: TreeNode | null
right: TreeNode | null
constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
this.val = (val===undefined ? 0 : val)
this.left = (left===undefined ? null : left)
this.right = (right===undefined ? null : right)
}
}
function maxPathSum(root: TreeNode| null,hasRoot?:boolean): number {
if(!root){
return 0;
}
const leftVal=maxPathSum(root.left);
const rightVal=maxPathSum(root.right);
const leftValHasRoot=maxPathSum(root.left,true);
const rightValHasRoot=maxPathSum(root.right,true);
const hasRootArr=[root.val,
root.val+leftValHasRoot,
root.val+rightValHasRoot,
]
if(hasRoot){
return Math.max(...hasRootArr);
}
return Math.max(...hasRootArr,leftVal,rightVal,root.val+leftValHasRoot+rightValHasRoot);
};
8. 请尽可能详细地说明,React错误边界是什么?函数式组件能用吗?你的回答中不要写出示例代码。
React 错误边界(Error Boundaries)是一种特殊的 React 组件,用于捕获并处理其子组件树中的 JavaScript 错误。它们可以捕获并打印发生在其子组件中的错误,记录这些错误,并显示一个备用 UI,而不是让整个应用崩溃。
错误边界的用途
-
捕获子组件错误:
- 错误边界可以捕获并处理其子组件树中的 JavaScript 错误,防止错误传播到更高的组件层级,导致整个应用崩溃。
-
显示备用 UI:
- 当捕获到错误时,错误边界可以显示一个备用 UI,提供更好的用户体验,而不是显示一个空白的页面或浏览器的错误信息。
-
记录错误:
- 错误边界可以记录错误信息,便于开发者调试和修复问题。
错误边界的限制
-
只能捕获子组件错误:
- 错误边界只能捕获其子组件树中的错误,不能捕获自身或父组件的错误。
-
不能捕获异步错误:
- 错误边界不能捕获异步操作(如
setTimeout
、requestAnimationFrame
)或服务端渲染(SSR)中的错误。
- 错误边界不能捕获异步操作(如
-
不能捕获事件处理函数中的错误:
- 错误边界不能捕获事件处理函数(如点击事件)中的错误,这些错误需要在事件处理函数内部处理。
函数式组件中的错误边界
在 React 中,错误边界只能通过类组件来实现,因为错误边界需要维护状态来记录是否发生了错误。函数式组件本身没有状态管理的能力,因此不能直接作为错误边界使用。
如何在函数式组件中使用错误边界
虽然函数式组件不能直接作为错误边界,但可以通过以下几种方式在函数式组件中使用错误边界:
-
包裹在类组件错误边界中:
- 将函数式组件包裹在一个类组件的错误边界中,利用类组件的错误边界来捕获和处理错误。
-
使用 React 16.6+ 的
ErrorBoundary
高阶组件(HOC):- 创建一个类组件的错误边界,并将其作为高阶组件(HOC)传递给函数式组件。
- 这样可以将错误边界的逻辑与函数式组件的逻辑分离,同时利用错误边界的功能。
总结
React 错误边界是一种特殊的类组件,用于捕获并处理其子组件树中的 JavaScript 错误。虽然函数式组件不能直接作为错误边界使用,但可以通过将其包裹在类组件的错误边界中或使用高阶组件(HOC)的方式,在函数式组件中利用错误边界的功能。