前端知识点心得

前端知识点心得

1.JS中的this指向谁?

1.什么是this?

  • 一个关键字,一个特殊的对象引用
  • this 由调用方式决定,运行时绑定
  • this的指向是动态的,它的值取决于函数被调用的方式(简称谁调用它,它就指向谁)

2.this的六条核心规则
1. 默认绑定
当函数独立调用(非对象方法、非事件、非构造函数等),this 默认指向 全局对象。
- 非严格模式下:全局对象(window)
- 严格模式下:undefined
2. 隐式绑定(方法调用)
当函数作为对象的方法被调用时,this 指向 调用该方法的对象:

1
2
3
4
5
6
7
const person = {
name: 'John',
sayHi(){
console.log(this.name); // this 指向 person 对象
}
}
person.sayHi(); // "John"

注意:隐式丢失陷阱
如果方法被赋值给变量后调用,this 会丢失原对象指向:
1
2
const func = person.sayHi;
func(); // 输出 undefined(严格模式报错)

3. 显示绑定
场景:通过特定的方法强制指定this
方法:call()、apply()、bind()
1
2
3
4
5
6
7
8
9
10
11
12
13
function greet() {
console.log(`Hello, ${this.name}`);
}

const user = { name: 'Bob' };

// 1. call/apply 立即调用
greet.call(user); // 输出 "Hello, Bob"
greet.apply(user); // 同上

// 2. bind 返回新函数
const boundFunc = greet.bind(user); // this 指向绑定user创建的新函数boundFunc
boundFunc(); // 输出 "Hello, Bob"

4. new绑定
场景:构造函数调用,this 指向新创建的对象
1
2
3
4
5
6
7
function Person(name){
// this = {}; (隐式)
this.name = name;
// return this; (隐式)
}
const jerry = new Person('Jerry');
console.log(jerry.name); // "Jerry"

5. 箭头函数的 this 指向
没有自己的this绑定
箭头函数的 this 继承自外层作用域,且无法被修改
this 捕获的是定义时的环境,而非调用时的环境
1
2
3
4
5
6
7
const obj = {
name: 'Dave',
sayHi: () => {
console.log(this.name); // this 指向外层(此处是 window)
}
};
obj.sayHi(); // 输出 undefined(浏览器中 window.name 为空)

6. DOM 事件中的 this
在 DOM 事件处理函数中,this 指向 触发事件的元素
1
2
3
button.addEventListener('click', function() {
console.log(this); // 输出 <button> 元素
});

this绑定规则优先级: new > 显示 > 隐式 > 默认

2.JS柯里化函数

1.什么是柯里化函数?(Currying)
柯里化是一种将多参数函数转换成一系列单参数函数的技术,它让函数变得更灵活、更易于组合。

2.柯里化的核心特点:

  • 参数分解:每次只接受一个参数,返回新函数处理后续参数
    多参数 -> 单参数序列
  • 延迟执行(直到所有参数收集完毕才执行最终计算)、复用性、组合性

3.柯里化的实际用途:

  • 参数复用,动态创建函数,函数组合
    1
    2
    3
    4
    5
    6
    // 创建通用的"问候"函数
    const greet = (greeting) => (name) => `${greeting}, ${name}!`;

    const sayHello = greet('Hello');
    sayHello('Alice'); // "Hello, Alice!"
    sayHello('Bob'); // "Hello, Bob!"

4.柯里化函数实现步骤:

  1. 定义一个接受多个参数的函数
  2. 定义一个返回值为一个函数的函数,接收一个参数(也可接受多个,但是接受多个那为啥还要用柯里化呢)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 普通加法函数
    function add(a, b, c) {
    return a + b + c;
    }

    // 柯里化转换函数
    function curry(fn) {
    return function curried(...args) {
    if (args.length >= fn.length) {
    return fn.apply(this, args);
    } else {
    return function(...args2) {
    return curried.apply(this, args.concat(args2));
    };
    }
    };
    }

    // 使用
    const curriedAdd = curry(add);
    console.log(curriedAdd(1)(2)(3)); // 6

3.什么是闭包?

闭包就是一个函数可以访问其外部函数及其词法作用域内的变量。

4.深入剖析 useEffect 依赖项数组

useEffect是一个函数式Hook,允许我们在函数式组件中做一些副作用操作(数据获取、设置订阅、手动更新DOM操作等),依赖项数组是第二个参数,主要作用是控制useEffect的执行时机,只有当数组中的值发生变化时,useEffect才会执行。

如果没有传递第二个参数,useEffect在首次渲染和之后的每次更新时,都会执行一次定义个回调函数。

如果传递空数组[],useEffect只会在组件首次渲染时执行一次回调函数,不会在组件更新时执行。如果返回了一个清理函数,那么也会在组件卸载时执行一次。

问:useEffect 的回调函数可以是 async?为什么?
答:不可以。async 函数隐式返回 promise,useEffect 期望回调不返回或者返回清理函数,promise 会被当做清理函数。

问:如何在useEffect中执行异步操作,比如获取数据?
答:在回调函数内部定义一个async函数,然后立即调用它,这样就能确保useEffect接收的回调是同步的,这个同步回调内部自己处理了异步操作。

1
2
3
4
5
6
7
8
9
10
11
// ✅ 正确写法:在 useEffect 内部定义并立即调用异步函数
useEffect(() => {
// 1. 定义一个异步函数
const fetchData = async () => {
const result = await myApi.fetch('/api/data');
setData(result);
};

// 2. 立即调用它
fetchData();
}, []); // 依赖项数组

然而,上面的基础写法忽略了一个重要问题:竞态条件(Race Condition)。

竞态条件(Race Condition) 指的是两个或多个操作(进程、线程、任务)竞争同一份资源,但最终的执行结果取决于它们执行的精确先后顺序,而这种顺序是不确定的,从而导致无法预测的、常常是错误的的行为。

如果组件在数据请求完成之前就被卸载了,或者 useEffect 因为 props 或 state 变化而再次执行,那么第一次请求返回后调用 setData 会更新一个已卸载的组件,导致 React 报内存泄漏警告。

因此,更专业、更完整的写法需要包含清理(Cancellation)机制。

方案一:使用标志位(Abort Flag)
这是最经典的解决方案,利用组件卸载时 useEffect 的清理函数来阻止设置状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
useEffect(() => {
let isMounted = true; // 1. 定义一个标志位,表示组件是否挂载

const fetchData = async () => {
const result = await myApi.fetch('/api/data');
if (isMounted) { // 2. 只有在组件仍挂载时才更新状态
setData(result);
}
};

fetchData();

return () => {
isMounted = false; // 3. 清理函数:组件卸载时将标志位设为 false
};
}, []);

方案二:使用 AbortController(现代浏览器和 Node.js 支持)
对于真正的网络请求(如 fetch),你可以使用 AbortController 来直接取消请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
useEffect(() => {
const abortController = new AbortController(); // 1. 创建控制器

const fetchData = async () => {
try {
const response = await fetch('/api/data', {
signal: abortController.signal, // 2. 将信号注入请求
});
const result = await response.json();
setData(result);
} catch (error) {
if (error.name === 'AbortError') {
console.log('请求被取消'); // 捕获因取消产生的错误
} else {
// 处理其他真正的错误
}
}
};

fetchData();

return () => {
abortController.abort(); // 3. 清理函数:取消进行中的请求
};
}, []);

5.useTransitionuseDeferredValue 如何优化用户体验?有何区别?

  • useTransition: 标记“不那么紧急”的更新
    允许将状态更新标记为过渡(transition),降低其优先级
    返回isPending(布尔值,表示过渡是否待处理)和startTransition(函数,用于启动过渡,包裹低优先级状态更新)
  • useDeferredValue: 获取一个“延迟”的值
    接受一个值,并返回该值的“延迟”版本,只有当组件紧急更新(比如用户输入)完成后才会更新
    核心:提供一个值的“副本”,此副本的更新被推迟,以避免阻塞主渲染进程
  • useTransition应用场景:搜索 筛选大型列表
    因为你在搜索框输入的时候,应该是优先级最高的,比如你在输入时,搜索框底下的列表这时候开始渲染,但是你突然删除了之前的输入框搜索值,重新输入其它值搜索,假如你之前搜索的是一个很大的列表,你肯定不想看到旧的搜索结果还在渲染,你想获取最新的列表,这是就需要将搜索列表设为低优先级。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const [inputValue, setInputValue] = useState('');
    const [searchTerm, setSearchTerm] = useState('');
    const [isPending, startTransiton] = useTransition();

    const handleChange = (e) => {
    setInputValue(e.target.value); //立即更新
    startTransition(() => {
    setSearchTerm(e.target.value); //延迟更新
    })
    }
  • useDeferredValue应用场景:外部数据源的图表可视化
    图标组件接受频繁更新的data的prop,导致重绘耗时,引发卡顿。这可能就无法控制父组件更新data的时机。
    首选useDeferredValue:用于获取data prop的一个延迟版本
    1
    2
    3
    4
    5
    function Chart({ data }) {
    const deferredData = useDeferredValue(data); // 获取延迟版本
    // 使用 deferredData 进行渲染
    return <ExpensiveChartRender data={deferredData} />
    }
    关键区别:useTransition作用于状态更新的过程。
    useDeferredValue作用于一个具体的值。

6.React中的“状态提升”:优缺点是什么?它的边界在哪里?

核心思想:当多个组件要共享或反应同一份变化的数据时,将共享状态移至这些组件最近的共同父组件中。

目的:1.这些组件都可以访问同一个数据源
2.使多个组件可以反应相同的数据变化
3.保持数据流的单向性与可预测性

缺点:1.Prop Drilling(属性逐层传递):状态可能需要通过许多中间层组件传递;中间组件被迫接受并传递他们本身并不需要的props.
2.父组件膨胀:最近的共同父组件可能承载过多无关状态和逻辑,变得臃肿。
3.潜在的性能问题:父组件状态更新可能导致所有子组件重新渲染,导致性能问题,需配合React.memo等优化。

边界:适用场景:组件层级关系相对简单,少数几个组件共享状态,逻辑清晰,易于维护。但是碰到层级过深或全局状态等复杂场景就不适用了。
替代方案:Context API、状态管理库(Redux、Zustand、Jotai等)

7.如何用 useContext + useReducer 实现一个轻量级的状态管理器?

useContext 核心API:
React.createContext(): 创建一个上下文(context)对象
Context.Provider: 数据提供者,其 value 属性可被后代消费者获取
Context.Consumer/useContext: 数据消费者

useReducer 核心API:
[state, dispatch] = useReducer(reducer, initialArg, init?)
reducer 函数:(currentState, action) => newState
dispatch 函数:触发状态更新,传递action对象。

示例:创建一个全局的计数器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const initialState = { count: 0 };

function reducer(atate, action){
switch(action.type){
case 'INCREMENT':
return {...state, count: state.count + 1 }
case 'DECREMENT':
return {...state, count: state.count - 1 }
case 'RESET':
return {...state, count: 0 }
default:
return state
}
}

const CountContext = createContext();

export function CounterProvider({ children }){
const [state, dispatch] = useReducer(reducer, initialState);
return (
<CounterContext.Provider value={{ state, dispatch }}>
{children}
</CounterContext.Provider>
)
}

export function useCounter(){
const context = useContext(CountContext);
if(!context) throw new Error('useCounter must be used within a CountProvider');
return context;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { CounterProvider, useCounter } from './store';

function DisplayCounter(){
const { state } = useCounter();
return <p>Count: {state.count}</p>
}

function Controls(){
const { dispatch } = useCounter();
return (
<>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
<button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
</>
)
}

funciton App(){
return (
<CounterProvider>
<h1>Counter App</h1>
<DisplayCounter />
<Controls />
</CounterProvider>
)
}

8.如何在 React 中实现状态的持久化存储?

什么是状态持久化?

核心概念:指将 React 组件或应用的状态数据存储在浏览器之外或浏览器会话之外的某个地方,使得在页面刷新、关闭后依然能够恢复之前的状态。

关键目的:提升用户体验,避免数据丢失。

方法一:使用Web Storage API
localStorage:
数据永久存储,除非用户手动清除浏览器缓存或代码主动删除。
同源下的所有标签页和窗口共享数据。
容量较大(通常为5-10MB)。
sessionStorage:
数据仅在当前浏览器会话期间有效,关闭标签页或浏览器后数据被清除。
数据仅在当前标签页内可见,不同标签页不共享。
容量与localStorage类似。
方法二:封装自定义Hook(usePersistentState)
目的:将持久化逻辑抽象出来,提高复用性,简化组件代码。
核心思路:
1.内部依然使用 useState 和 useEffect。
2.接受key(存储键名)和 initialValue。
3.返回与 useState 类似的 [state, setState] 数组。
方法三:使用状态管理库的持久化插件
– 场景:项目中已使用如 Redux、Zustand、Recoil 等状态管理库。
– 常用插件:
Redux Persist: 配合Redux使用,可以配置存储引擎(默认localStorage)、黑白名单等。
Zustand Middleware: Zustand提供了persist中间件。
Recoil Persist: 社区提供的Recoil持久化方案。
– 优点:集成方便,配置灵活,通常以处理号序列化等问题。
– 缺点:引入额外依赖,增加包体积。

9.一个简单的案例看懂useReducer的用法

state:当前的状态值,和 useState 返回的一样。

dispatch:一个“派发”函数,你用它来发送操作指令(Action)。

reducer:一个函数,它决定了如何根据操作指令来更新状态。

initialState:状态的初始值。

一个简单的计数器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { useReducer } from 'react';

// 1. 定义reducer函数(状态更新逻辑)
function counterReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'SET':
return { count: action.payload }; // 使用action带来的数据
default:
return state;
}
}

function Counter() {
// 2. 使用useReducer,传入reducer和初始状态
const [state, dispatch] = useReducer(counterReducer, { count: 0 });

return (
<div>
<p>Count: {state.count}</p>
{/* 3. 通过dispatch发送action来更新状态 */}
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
<button onClick={() => dispatch({ type: 'SET', payload: 100 })}>
Set to 100
</button>
</div>
);
}
export default Counter;

10.useRef的用法

1.操作真实DOM:

1
2
3
4
5
6
7
8
9
10
11
import { useRef, useEffect } from "react";

function InputFocus() {
const inputRef = useRef(null); // 创建 ref

useEffect(() => {
inputRef.current.focus(); // 页面加载后聚焦
}, []);

return <input ref={inputRef} placeholder="自动聚焦" />;
}

2.保存不会触发渲染的变量:

1
2
3
4
5
6
7
8
9
10
function Timer() {
const countRef = useRef(0);

const handleClick = () => {
countRef.current += 1;
console.log("点击次数:", countRef.current);
};

return <button onClick={handleClick}>点我</button>;
}

适合保存中间状态、定时器 ID、前一次的值等。

[up主专用,视频内嵌代码贴在这]