【未完待续】React高频面试题

React 核心知识点详解

本文档详细解答 React 面试中的核心问题,涵盖 useEffect、async/await、防抖函数等知识点。


1. React中,防抖函数、业务函数、useCallback的包裹顺序应该是什么样子,为什么

正确的包裹顺序

scss 复制代码
// ❌ 错误顺序
const debouncedFn = debounce(callback, 300);
const handler = useCallback(debouncedFn, [deps]);

// ✅ 正确顺序
const handler = useCallback(
  debounce((value) => {
    // 业务逻辑
  }, 300),
  [deps] // 依赖项
);

// ✅ 或者更清晰的方式
const handler = useMemo(
  () => debounce((value) => {
    // 业务逻辑
  }, 300),
  [deps]
);

为什么这样包裹?

原因分析:

  1. useCallback 的作用:缓存函数引用,避免每次渲染创建新函数
  2. debounce 的作用:包装函数,返回一个防抖后的新函数
  3. 执行时机问题
    • 如果 debounceuseCallback 外面,每次渲染都会执行 debounce,导致防抖函数被重复创建
    • 这样会失去防抖效果,因为每次都是新的防抖实例

最佳实践

scss 复制代码
function SearchComponent() {
  const [query, setQuery] = useState('');

  // ✅ 使用 useMemo 缓存防抖函数(推荐)
  const debouncedSearch = useMemo(
    () => debounce((value) => {
      console.log('搜索:', value);
    }, 300),
    [] // 空依赖,整个组件生命周期只创建一次
  );

  // ✅ 或者使用 useCallback
  const handleSearch = useCallback(
    debounce((value) => {
      console.log('搜索:', value);
    }, 300),
    []
  );

  // ✅ 如果需要访问最新的 state,使用 ref
  const queryRef = useRef(query);
  queryRef.current = query;

  const debouncedSearch = useMemo(
    () => debounce((value) => {
      console.log('搜索:', queryRef.current, value);
    }, 300),
    []
  );

  return <input onChange={(e) => debouncedSearch(e.target.value)} />;
}

清理防抖函数

scss 复制代码
useEffect(() => {
  return () => {
    debouncedSearch.cancel(); // 组件卸载时取消 pending 的防抖调用
  };
}, [debouncedSearch]);

2. useEffect的第二个参数的用法和使用场景

三种用法

javascript 复制代码
// 1. 不传第二个参数 - 每次渲染后都执行
useEffect(() => {
  console.log('每次渲染后执行');
});

// 2. 传入空数组 [] - 仅在挂载时执行一次
useEffect(() => {
  console.log('组件挂载时执行一次');
  return () => console.log('组件卸载时执行');
}, []);

// 3. 传入依赖数组 [dep1, dep2] - 仅在依赖变化时执行
useEffect(() => {
  console.log('count 变化了:', count);
}, [count]);

使用场景详解

场景 1:数据获取(挂载时)
scss 复制代码
useEffect(() => {
  fetchUserData();
}, []); // 空数组 = 仅挂载时执行
场景 2:响应状态变化
ini 复制代码
useEffect(() => {
  document.title = `点击了 ${count} 次`;
}, [count]); // count 变化时更新标题
场景 3:监听多个依赖
scss 复制代码
useEffect(() => {
  fetchSearchResults(query, page);
}, [query, page]); // query 或 page 变化时重新搜索
场景 4:监听 props 变化
scss 复制代码
function UserProfile({ userId }) {
  useEffect(() => {
    fetchUser(userId);
  }, [userId]); // userId 变化时重新获取用户信息
}
场景 5:订阅/取消订阅
scss 复制代码
useEffect(() => {
  const subscription = someObservable.subscribe(handler);
  
  return () => {
    subscription.unsubscribe(); // 清理订阅
  };
}, []); // 空数组表示仅在挂载/卸载时执行

依赖项的注意事项

scss 复制代码
// ❌ 错误:遗漏依赖
useEffect(() => {
  fetchData(userId, filter); // filter 变化时不会重新执行
}, [userId]); // 缺少 filter

// ✅ 正确:包含所有依赖
useEffect(() => {
  fetchData(userId, filter);
}, [userId, filter]);

// ✅ 使用 ESLint 插件自动检测
// npm install eslint-plugin-react-hooks --save-dev

3. useEffect中回调函数中的return出去的是什么,有什么作用

返回值:清理函数

useEffect 返回的函数被称为 **清理函数(cleanup function) **,用于清理副作用。

执行时机

javascript 复制代码
useEffect(() => {
  console.log('1. 副作用执行');
  
  return () => {
    console.log('2. 清理函数执行');
  };
}, [count]);

执行顺序:

  1. 组件挂载 → 执行副作用函数
  2. 依赖变化 → 先执行清理函数 → 再执行新的副作用函数
  3. 组件卸载 → 执行清理函数

典型应用场景

场景 1:取消订阅
javascript 复制代码
useEffect(() => {
  const subscription = eventEmitter.subscribe((data) => {
    console.log('收到数据:', data);
  });
  
  return () => {
    subscription.unsubscribe(); // 清理订阅,防止内存泄漏
  };
}, []);
场景 2:清除定时器
javascript 复制代码
useEffect(() => {
  const timer = setInterval(() => {
    console.log('定时执行');
  }, 1000);
  
  return () => {
    clearInterval(timer); // 清除定时器
  };
}, []);
场景 3:取消网络请求
ini 复制代码
useEffect(() => {
  const controller = new AbortController();
  
  fetch(url, { signal: controller.signal })
    .then(res => res.json())
    .then(data => setData(data));
  
  return () => {
    controller.abort(); // 取消请求
  };
}, [url]);
场景 4:事件监听器清理
javascript 复制代码
useEffect(() => {
  const handleResize = () => {
    console.log('窗口大小变化');
  };
  
  window.addEventListener('resize', handleResize);
  
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

4. 在两次更新中,useEffect第一个参数,第一个参数中的返回值,第二个参数的执行时机是什么样子

执行顺序图解

假设组件经历以下生命周期:

  1. 挂载
  2. 更新(count 从 0 → 1)
  3. 更新(count 从 1 → 2)
  4. 卸载
javascript 复制代码
useEffect(() => {
  console.log('A. 副作用执行');
  
  return () => {
    console.log('B. 清理函数执行');
  };
}, [count]);

执行顺序

less 复制代码
【第一次挂载】
  → A. 副作用执行

【第一次更新 (count: 0 → 1)】
  → B. 清理函数执行 (清理上一次的副作用)
  → A. 副作用执行 (执行新的副作用)

【第二次更新 (count: 1 → 2)】
  → B. 清理函数执行 (清理上一次的副作用)
  → A. 副作用执行 (执行新的副作用)

【卸载】
  → B. 清理函数执行 (最后清理)

详细示例

ini 复制代码
function Timer({ count }) {
  useEffect(() => {
    console.log(`[${count}] 副作用: 创建定时器`);
    const timer = setInterval(() => {
      console.log(`[${count}] 定时器运行中...`);
    }, 1000);
    
    return () => {
      console.log(`[${count}] 清理: 清除定时器`);
      clearInterval(timer);
    };
  }, [count]);
  
  return <div>{count}</div>;
}

/* 执行日志:
[0] 副作用: 创建定时器
[0] 定时器运行中...
[0] 定时器运行中...

[更新: count = 1]
[0] 清理: 清除定时器    ← 先清理旧的
[1] 副作用: 创建定时器   ← 再创建新的
[1] 定时器运行中...

[更新: count = 2]
[1] 清理: 清除定时器
[2] 副作用: 创建定时器

[卸载]
[2] 清理: 清除定时器
*/

关键点总结

  1. 清理函数在下次副作用执行前调用(不是在下次渲染前)
  2. 清理函数能读取到上一次渲染的 props 和 state(闭包)
  3. 挂载时不执行清理函数,卸载时执行清理函数

5. useEffect中怎么使用async

❌ 错误写法

scss 复制代码
// 错误:useEffect 回调不能是 async 函数
useEffect(async () => {
  const data = await fetchData(); // ❌ 这样写会报警告
  setData(data);
}, []);

// 原因:useEffect 期望返回清理函数或 undefined
// async 函数返回 Promise,这会导致问题

✅ 正确写法 1:立即执行函数(IIFE)

scss 复制代码
useEffect(() => {
  (async () => {
    try {
      const data = await fetchData();
      setData(data);
    } catch (error) {
      console.error('获取数据失败:', error);
    }
  })();
}, []);

✅ 正确写法 2:在内部定义 async 函数并调用

scss 复制代码
useEffect(() => {
  const loadData = async () => {
    try {
      const data = await fetchData();
      setData(data);
    } catch (error) {
      console.error(error);
    }
  };
  
  loadData();
}, []);

✅ 正确写法 3:使用 .then() 链式调用

ini 复制代码
useEffect(() => {
  fetchData()
    .then(data => setData(data))
    .catch(error => console.error(error));
}, []);

✅ 完整示例(带清理功能)

ini 复制代码
useEffect(() => {
  let isMounted = true; // 标记组件是否挂载
  const controller = new AbortController();
  
  const loadData = async () => {
    try {
      const response = await fetch(url, {
        signal: controller.signal
      });
      const data = await response.json();
      
      if (isMounted) { // 只有组件还在时才更新状态
        setData(data);
      }
    } catch (error) {
      if (error.name !== 'AbortError' && isMounted) {
        console.error('请求失败:', error);
      }
    }
  };
  
  loadData();
  
  return () => {
    isMounted = false; // 标记组件已卸载
    controller.abort(); // 取消请求
  };
}, [url]);

6. async...await语法,如果函数不用async标识,内部直接使用await,会有什么后果

❌ 错误示例

csharp 复制代码
// ❌ 错误:非 async 函数中使用 await
function fetchData() {
  const response = await fetch('/api/data'); // SyntaxError
  return response.json();
}

// 报错:Uncaught SyntaxError: await is only valid in async functions

会产生什么后果?

  1. **语法错误(SyntaxError) **:JavaScript 引擎会抛出错误
  2. 代码无法执行:整个函数都不会运行
  3. 影响范围:如果这个函数被导出,会导致整个模块加载失败

为什么必须用 async 标识?

javascript 复制代码
// ✅ 正确写法
async function fetchData() {
  const response = await fetch('/api/data');
  return response.json();
}

// 等价于
function fetchData() {
  return fetch('/api/data')
    .then(response => response.json());
}

原因:

  • await 会暂停函数执行,等待 Promise resolve
  • 只有 async 函数才能以这种方式暂停执行
  • async 函数总是返回 Promise,即使没有显式 return

深入理解 async/await 转换

javascript 复制代码
async function example() {
  console.log('开始');
  const result = await someAsyncOperation();
  console.log('结果:', result);
  return result;
}

// Babel 转换后(简化版):
function example() {
  return new Promise((resolve, reject) => {
    console.log('开始');
    someAsyncOperation()
      .then(result => {
        console.log('结果:', result);
        resolve(result);
      })
      .catch(reject);
  });
}

特殊场景:顶层 await(Top-level await)

javascript 复制代码
// ✅ 在 ES2022+ 的模块中,支持顶层 await
// 无需包裹在 async 函数中

const response = await fetch('/api/data'); // ✅ 合法
const data = await response.json();

export default data;

注意:

  • 仅支持在 ES Module(.mjs 或 package.json 中 type: "module")中使用
  • CommonJS 模块不支持顶层 await

7. const a = 1, a.toString()的结果是什么,为什么

结果

vbscript 复制代码
const a = 1;
a.toString(); // "1"(字符串 "1")

为什么是这个结果?

原因分析
  1. 原始值没有方法
    • 1 是原始值(primitive value),不是对象,理论上没有 .toString() 方法
    • 但 JavaScript 允许在原始值上调用方法
  1. **自动装箱(Autoboxing) **:
    • 当在原始值上调用方法时,JavaScript 会自动将其转换为对应的包装对象
    • 1new Number(1) → 调用 Number.prototype.toString()
    • 调用完成后,临时对象被销毁
  1. Number.prototype.toString() 的行为
    • 将数字转换为字符串表示
    • 1"1"
执行过程详解
vbscript 复制代码
const a = 1;
a.toString();

/* 内部执行过程:
1. 访问 a.toString
   → JavaScript 发现 a 是原始值
   → 创建临时对象:new Number(1)
   
2. 调用 toString 方法
   → Number.prototype.toString.call(new Number(1))
   
3. 返回结果
   → "1"(字符串)
   
4. 临时对象被垃圾回收
*/

验证自动装箱

arduino 复制代码
const a = 1;

// 尝试添加属性
a.customProp = 'test'; // ✅ 不报错,但无效
console.log(a.customProp); // undefined

// 原因:
// 1. a.customProp = 'test' 时,创建了临时 Number 对象
// 2. 属性被添加到临时对象上
// 3. 临时对象立即被销毁
// 4. 下次访问 a.customProp 时,又创建了新的临时对象
// 5. 新对象上没有 customProp,所以是 undefined

不同类型的 toString()

scss 复制代码
// Number
(1).toString();        // "1"
(1.5).toString();      // "1.5"
(10).toString(2);      // "1010" (二进制)
(10).toString(16);     // "a" (十六进制)

// String
"hello".toString();    // "hello"(原样返回)

// Boolean
true.toString();       // "true"
false.toString();      // "false"

// null 和 undefined(没有包装对象)
null.toString();       // ❌ TypeError
undefined.toString();  // ❌ TypeError

原始值 vs 包装对象

less 复制代码
// 原始值
const a = 1;
typeof a; // "number"

// 包装对象
const b = new Number(1);
typeof b; // "object"
b.valueOf(); // 1(获取原始值)

// 相等性比较
a == b;  // true(类型转换后比较)
a === b; // false(类型不同)

// typeof
typeof a.toString(); // "string"(返回的是字符串)
typeof b.toString(); // "string"

为什么这样设计?

JavaScript 借鉴了 Java 的设计,为了让原始值也能像对象一样使用方法:

  • 优点:代码简洁,不需要手动创建包装对象
  • 缺点:可能让人误以为原始值也是对象

性能优化:

  • 自动装箱只发生在访问属性/方法时
  • 不访问属性时,仍然是高效的原始值存储

总结

这 7 个问题涵盖了 React 的核心机制和 JavaScript 的基础原理:

  1. useCallback + debounce:理解包裹顺序,避免重复创建
  2. useEffect 依赖数组:掌握三种用法场景
  3. 清理函数:理解副作用清理的重要性
  4. 执行时机:深入理解 React 的生命周期
  5. async in useEffect:正确处理异步操作
  6. async/await 语法:理解 JavaScript 的异步机制
  7. 自动装箱:理解 JavaScript 的类型系统

建议结合实际项目多加练习,这些知识点在面试和实际开发中都非常重要!


文档创建时间:2026-04-17创建者:OpenClaw AI Assistant

相关推荐
m0_738120721 小时前
ctfshow靶场SSRF部分——基础绕过到协议攻击解题思路与技巧(一)
服务器·前端·网络·安全·php
counterxing1 小时前
AI Agent 做长任务,问题到底 出在哪?
前端·后端·ai编程
漂流瓶jz1 小时前
从TailwindCSS到UnoCSS:原子化CSS框架接入、特性与配置
前端·css·react.js
Mr_Swilder1 小时前
01:按步解析 —— 绘制固定三角形
前端
原鸣清2 小时前
Swift 面试高频五连问:Optional、Task、Actor、Concurrency 和 OC 差异
前端
前端Hardy2 小时前
谁还没⽤过shadcn/ui?114k+星标,不装NPM包,前端组件自由终于实现了
前端·javascript·vue.js
morestrive2 小时前
基于 fabric.js 实现浏览器端矢量 PDF 导出
前端·github
Bolt2 小时前
用 pnpm 11 省掉项目里的 .nvmrc 与 .npmrc
前端·npm·node.js
猪猪聪明_V2 小时前
前端码农的本地项目启动器
前端·javascript