【未完待续】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. 执行时机问题:
    • 如果 debounce 在 useCallback 外面,每次渲染都会执行 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 会自动将其转换为对应的包装对象
    • 1 → new 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

相关推荐
计算机魔术师1 小时前
管谷歌的不是Pichai一个人,是一整层AI智能体
前端
米米商聊1 小时前
图标能点,按钮就可用了吗?键盘、名称与收藏状态
前端·javascript
Younglina2 小时前
三个晚上,我和 WorkBuddy 做了个生图网站
前端·后端·ai编程
xy34532 小时前
Axure 9.0 内联框架加载方式
前端·ui·html·axure·原型·产品设计
陈随易3 小时前
副业搞钱点子之收入查看器
前端·后端
Profile排查笔记3 小时前
浏览器指纹逆向怎么分析:从网络请求追踪到 JavaScript 采集链
前端·人工智能·后端·自动化
hudie7653 小时前
LikeShop 二开最常改的 5 个文件(附改前改后对比)
前端·uni-app·php·个人开发
qetfw4 小时前
Bash 函数与流程控制:read、if、for、case 验证
linux·前端·javascript·bash
鬼手点金4 小时前
从 API 消息到模型 Token:Chat Template
java·前端·javascript·数据库·人工智能·深度学习·1024程序员节
Shadow(⊙o⊙)5 小时前
LangChain 自定义工具全解析:从 @tool 到 bind_tools 的完整调用链路
java·服务器·前端·1024程序员节