字节面试题
字节飞书一面
实现一个具有状态的倒计时组件(支持暂停、继续)你怎么考虑
“好的,我将实现一个 React 函数组件形式的倒计时器,主要功能包括:
接收一个初始时间(比如 60 秒)
维护两个核心状态:剩余时间、是否正在运行
使用 useEffect 监听状态变化,启动或清除定时器,每秒减少剩余时间
提供三个按钮:开始/继续、暂停、重置
在倒计时结束时自动停止,并可展示提示
注意在组件卸载时清除定时器,防止内存泄漏
我会使用 React Hooks(useState、useEffect、useRef)来实现,逻辑清晰且符合 React 最佳实践。”
❓ 1. 如果我想在倒计时结束时触发一个回调(比如提醒、跳转、提交),怎么实现?
你可以增加一个
onEnd
回调 prop,在 timeLeft === 0 且状态变为停止时调用它。比如:
useEffect(() => {
if (timeLeft === 0 && !isActive) {
if (onEnd) onEnd();
}
}, [timeLeft, isActive, onEnd]);
❓ 2. 如果我要支持自定义时间格式(如 mm:ss、hh:mm:ss)怎么处理?
你可以提取一个
formatTime
函数,根据剩余秒数计算出 分钟 和 秒,然后用 padStart 格式化显示。比如:
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
❓ 3. 如果倒计时过程中,用户刷新了页面,如何保持倒计时状态不丢失?
这是一个进阶问题,涉及到状态持久化。
可以使用如下方案:
localStorage / sessionStorage 保存剩余时间和开始时间戳
使用定时器补偿机制,根据当前时间和目标时间差动态计算剩余时间
或者将状态保存到 全局状态管理(如 Redux / Zustand) 或 URL 参数 / 后端
❓ 4. 如果我传入的是分钟而不是秒,怎么处理?
可以在组件内部做单位转换,比如将分钟乘以 60 转为秒,或者直接接收秒数,让调用方传入
minutes * 60
。推荐做法是 保持内部统一用秒计算,对外提供清晰的 props 说明,比如:
<CountdownTimer initialTime={5 * 60} /> // 5 分钟
❓ 5. 如果我想让倒计时组件支持暂停后继续精确计时(比如用了 setTimeout 而不是 setInterval),有什么优缺点?
这是一个考察你对定时器精度、性能及实现方式理解的点。
setInterval
简单,但可能存在累计误差更精确的方式是用 递归的 setTimeout,每次在回调里再设置下一个 timeout
或者使用 时间戳差值计算(推荐用于高精度倒计时,比如考试系统)
❓ 6. 如何做单元测试?你会测试哪些点?
考察你的测试意识
可测试点包括:
初始时间是否正确显示
点击开始后是否倒计时
暂停后时间是否不变
重置后是否恢复初始时间
倒计时为 0 时是否停止
可使用 React Testing Library + Jest 进行测试
1.考察事件循环
2 闭包、作用域链、var变量提升
JavaScript 的三种作用域(全局、函数、块级)是词法作用域(静态作用域)在实际代码中的具体表现形式。换句话说:词法作用域是 JavaScript 作用域的设计原则,而这三种作用域是这个原则在不同场景下的具体应用。
术语 | 含义 | 说明 |
---|---|---|
全局作用域 | 在任何函数/代码块之外声明的变量/函数,属于全局作用域 | 全局可访问(如浏览器中挂载到 |
函数作用域 | 由 |
|
块级作用域 | 由 | ES6 引入,解决了 var 的变量泄露问题 |
词法作用域 | 变量和函数的作用域能访问哪些内容,是由它们在代码中的位置(书写位置)决定的,而不是运行的位置 | 也叫 静态作用域,是 JavaScript 采用的作用域模型 |
作用域链 | 当访问一个变量时,JS 会从当前作用域开始查找,逐级往外层作用域查找,直到全局作用域,这个查找的链条就叫作用域链 | 是词法作用域的运行时体现 |
知道闭包吗,他底层原理是什么(词法作用域)
闭包是指:一个函数可以“记住并访问”它被创建时的词法作用域,即使这个函数在其词法作用域之外执行。
换句话说:
当一个内部函数引用了外部函数的变量,且这个内部函数在外部函数执行完毕后依然可以被访问,就形成了闭包。
function outer() {let count = 0; // 外部函数的局部变量function inner() { // 内部函数count++;console.log(count);}return inner; // 返回内部函数
}const myFunc = outer(); // outer 执行完毕,按理说 count 应该被销毁
myFunc(); // 1
myFunc(); // 2
myFunc(); // 3
🔍 问题来了:outer() 执行完后,它的内部变量 count
按理应该被垃圾回收了,为什么 myFunc()
还能访问并修改它?
✅ 答案就是:闭包!inner 函数“记住了”它被创建时的作用域,即使 outer 已经执行完毕。
底层原理是什么?
✅ 闭包的本质,是由 JavaScript 的 词法作用域机制 决定的。他的形成要有词法作用域 + 函数嵌套 + 函数被外部引用(内部函数 “携带” 了它出生时的作用域(词法作用域),形成了闭包。)
从 JavaScript 引擎(如 V8)的视角来看:
函数在定义时,会记录它所在的词法环境(Lexical Environment)
这个环境包含了它所能访问的所有变量、作用域链
即使外部函数执行完毕,只要内部函数还被引用(比如被返回、赋值给全局变量等),
那么 这个内部函数依然持有对那个词法环境的引用
相应的变量(比如
count
)不会被垃圾回收这就是闭包能够“记住”外部变量的根本原因:词法作用域 + 引用保持
词法作用域(也叫静态作用域)是指:变量的可访问性是由代码中函数声明的位置(即词法位置)决定的,而不是由函数调用的位置决定。
函数在定义的时候,就已经确定了它能访问哪些变量,这些变量来自它外部的作用域,跟函数在哪执行无关。
对比项 | 词法作用域(Lexical Scope) | 动态作用域(Dynamic Scope) |
---|---|---|
作用域在何时确定 | 代码写出来的时候(定义时)就决定了 | 函数调用的时候才决定 |
JavaScript 是否采用 | ✅ JavaScript 采用的是词法作用域 | ❌ JavaScript 不采用动态作用域 |
典型语言 | JS、Python、C、Java 等 | Bash、Perl(部分)等 |
let a = 10;function foo() {console.log(a); // 这里访问的 a,是由词法位置决定的
}function bar() {let a = 20;foo(); // 输出 10,不是 20!因为 foo 定义时词法作用域是全局
}bar();
即使
foo()
是在bar()
内部调用的,它依然访问的是 定义时的那个作用域中的 a(全局的 10),而不是调用时的 a(bar 中的 20)。
这就是 词法作用域:函数定义时就已经确定了它能看到哪些变量。
闭包的常见应用场景
场景 | 说明 | 例子 |
---|---|---|
✅ 数据私有化(封装) | 用闭包模拟私有变量 | 比如计数器、缓存 |
✅ 函数工厂 | 返回一组有特定行为的函数 | 比如创建多个计数器 |
✅ 事件监听 / 回调 | 事件函数中引用外部变量 | 比如点击计数 |
✅ 防抖 / 节流 | 函数内引用定时器或上次执行时间 | 经典闭包应用 |
✅ 模块模式(Module Pattern) | 模拟模块作用域,暴露公共接口 | 比如 IIFE + 闭包 |
✅ 例子:用闭包实现私有变量
function createCounter() {let count = 0; // 私有变量,外部无法直接访问return {increment: function () {count++;console.log(count);},decrement: function () {count--;console.log(count);},};
}const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
// 外部无法直接访问 count,只能通过暴露的方法操作
闭包的注意事项(面试高频 follow-up)
问题 | 说明 |
---|---|
❗ 内存泄漏风险 | 闭包会阻止外部函数的变量被垃圾回收,如果滥用可能导致内存占用过高 |
✅ 如何避免? | 不再需要的闭包及时解除引用(比如设为 null) |
❗ 变量捕获的是引用,不是值 | 闭包捕获的是变量的引用,循环中常见 bug(比如 for 循环 + setTimeout) |
❗ 循环中常见闭包陷阱(面试常考!)
因为 var 没有块级作用域,循环中的 i 是共享的,而 setTimeout 是异步执行的,当回调函数运行时,循环早已结束,此时 i 的值已经变成 3,所以所有回调打印的都是 3。使用 let 或 IIFE 可以为每次循环创建独立的作用域,从而正确捕获每次的 i 值。
for (var i = 0; i < 3; i++) {setTimeout(function () {console.log(i); // 输出 3, 3, 3,不是 0, 1, 2}, 1000);
}
var
是 函数作用域(或全局作用域),不是块级作用域
let
才是 块级作用域(ES6 引入,推荐在循环中使用)所以这里的
i
实际上是挂载在 当前所在函数作用域(或全局) 下的一个变量,整个循环共享同一个 i
setTimeout是异步的,他
的回调函数 不会立即执行,而是被放入 任务队列,等待当前同步代码执行完毕后,大约 1 秒后才会执行重要的是:当 setTimeout 的回调函数执行时,for 循环早就已经执行完毕了!
同步代码执行过程(for 循环部分):
i = 0:进入循环,注册一个 setTimeout,回调函数被安排到 稍后执行
i = 1:进入循环,注册一个 setTimeout,回调函数被安排到 稍后执行
i = 2:进入循环,注册一个 setTimeout,回调函数被安排到 稍后执行
i = 3:不满足
i < 3
,循环终止 ✅⚠️ 此时,变量 i 的值已经是 3!
然后,过了约 1 秒后...
异步代码执行过程(setTimeout 回调):
你注册了 3 个 setTimeout 回调函数
它们几乎同一时间(延时结束后)依次执行
每个回调函数中都去访问变量
i
但此时它们访问的
i
是 同一个 i,值已经是 3
🔧 解决方法:使用 let 或 IIFE 创建新作用域
// 方法1:使用 let(块级作用域)
for (let i = 0; i < 3; i++) {setTimeout(() => console.log(i), 1000); // 0, 1, 2
}// 方法2:使用 IIFE 闭包
for (var i = 0; i < 3; i++) {(function (j) {setTimeout(() => console.log(j), 1000);})(i);
}
1.原型链
2.怎么理解普通函数 this指向
调用方式 | this 指向 | 是否常见 | 备注 |
---|---|---|---|
直接调用: | 全局对象(浏览器:window) | ✅ 常见 | 容易出错的地方 |
作为对象方法: | 调用该方法的对象(obj) | ✅ 常见 | this 永远指向调用者 |
构造函数调用: | 新创建的实例对象 | ✅ 常见 | 用于面向对象 |
call / apply / bind | 手动指定的第一个参数(对象) | ✅ 常用 | 显式绑定 this |
new绑定优先级 > 显示绑定优先级 > 隐式绑定优先级 > 默认绑定优先级
1.默认:
严格模式下,不能将全局对象用于默认绑定,this会绑定到undefined
,只有函数运行在非严格模式下,默认绑定才能绑定到全局对象window。
立即执行函数this必定指向window,定时器this指向window
2.隐式绑定:函数还可以作为某个对象的方法调用,这时this
就指这个上级对象
this
永远指向的是最后调用它的对象,注意独立调用和隐式丢失
这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this
指向的也只是它上一级的对象!!!!!!!
-
你是在调用
o
对象中的b
属性(它是一个对象),然后通过b
对象去调用它内部的fn
方法:b.fn()
所以,真正调用函数fn
的是b
对象,不是o
,也不是全局对象!o只是一个外层容器。
var o = {a:10,b:{fn:function(){console.log(this.a); //undefined}}
}
o.b.fn(); //undefined b调用的他,b中没有定义
var fn = o.b.fn();
fn() // 指向的是window 也就是undefinedlet obj = {name: 'obj',foo: function () {console.log(this); //obj function test() {console.log(this); //window 为什么? 因为test独立调用 }test()}}obj.foo()
3.new绑定:通过构建函数new
关键字生成一个实例对象,此时this
指向这个实例对象
4.显示修改:apply()、call()、bind()
是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时this
指的就是这第一个参数
箭头函数
1.箭头函数没有自己的 this,它的 this 是在定义时继承自外层作用域(词法作用域)的 this,且这个 this 是固定的,不会因为调用方式改变。
2.箭头函数不能作为构造函数(使用new调用会报错),因为它没有自己的this和prototype;
3.箭头函数也没有arguments对象,需用剩余参数(...args)替代
适合场景举例:
-
数组方法:
arr.map(item => item * 2)
-
Promise 链:
.then(() => { ... })
-
简洁的回调函数:
button.addEventListener('click', () => { ... })
不适合场景举例:
-
对象方法(this 会指向错误) 、构造函数(不能 new)、需要动态 this 的地方(比如事件函数、定时器中需要访问组件实例)
const fn = (...args) => {console.log(args); // 类数组,类似 arguments
};
const obj = {name: 'Alice',normalFn: function () {console.log(this.name); // Alice},arrowFn: () => {console.log(this.name); // undefined(this 指向外层,通常是 window)}
};obj.normalFn(); // this === obj
obj.arrowFn(); // this !== obj!箭头函数 arrowFn 的 this,并不会指向 obj,因为 obj 本身并不是一个作用域(scope),它只是一个对象!
箭头函数中的 this,继承的是它被定义时所处的外层 JavaScript 作用域(词法作用域)中的 this,而 obj 是一个对象字面量,它不形成作用域!但是!obj是一个对象字面量,它不是一个函数,也不形成作用域!
对象字面量 { ... }中的代码块,并不会形成 JavaScript 的作用域!
在代码书写时就能确定 this
的指向(编译时绑定)
举个例子:
const obj = {sayThis: () => {console.log(this);}
};obj.sayThis(); // window 因为 JavaScript 没有块作用域,所以在定义 sayThis 的时候,里面的 this 就绑到 window 上去了
const globalSay = obj.sayThis;
globalSay(); // window 浏览器中的 global 对象
虽然箭头函数的this
能够在编译的时候就确定了this
的指向,但也需要注意一些潜在的坑
下面举个例子:
绑定事件监听
const obj = {name: 'Alice',start: function () {setTimeout(() => {console.log('Hello, ' + this.name); // this 是谁?}, 1000);}
};obj.start(); // 输出:Hello, Alice ✅这里用的是 箭头函数 作为 setTimeout 的回调
箭头函数没有自己的 this,它继承了外层函数 start 的 this,即 obj
所以 this.name === obj.name === 'Alice'
const button = document.getElementById('mngb');
button.addEventListener('click', ()=> {console.log(this === window) // truethis.innerHTML = 'clicked button'
})
上述可以看到,我们其实是想要this
为点击的button
,但此时this
指向了window
包括在原型上添加方法时候,此时this
指向window
Cat.prototype.sayName = () => {console.log(this === window) //truereturn this.name
}
const cat = new Cat('mm');
cat.sayName()
3.call/apply/bind
11.TS解决什么问题
问题 | 说明 | TS 是如何解决的? |
---|---|---|
1. JavaScript 是动态类型语言,没有编译时类型检查 | 变量类型在运行时才能确定,容易因类型错误引发 bug,且难以提前发现 | ✅ TS 引入了 静态类型系统,可以在编码阶段发现类型不匹配等问题 |
2. 代码可读性、可维护性差,尤其在大型项目中 | 没有明确的类型标注,别人(或未来的你)很难一眼看懂函数参数、返回值等的数据类型 | ✅ TS 支持 类型注解,让代码意图更清晰 |
3. IDE 支持较弱,自动补全和智能提示有限 | JS 的动态特性让编辑器难以推断变量类型,智能提示不精准 | ✅ TS 提供 强大的类型推导与 IDE 智能提示,提升开发效率 |
4. 重构困难,容易遗漏类型相关的改动 | 修改一个函数的参数类型或返回值,可能影响多个调用点,但 JS 无法提前预警 | ✅ TS 可以 在编译时发现类型不兼容,避免线上隐患 |
5. 团队协作时沟通成本高 | 没有类型标注时,团队成员需要靠文档或口头约定数据结构,容易产生歧义 | ✅ TS 通过 接口(interface)、类型(type)、泛型 等,明确数据契约 |
12.JS有类型检测吗
JavaScript 有运行时类型检测,但没有编译时(静态)类型检测。
也就是说:
-
JS 是动态类型语言:变量的类型是在运行时决定的,你可以在任何时候将一个变量赋值为不同类型的值
-
JS 自身提供了一些运行时类型检测的手段,比如
typeof
、instanceof
、===
等 -
但 JS 没有在代码编写阶段(静态阶段)进行类型检查的能力(除非你用了 TypeScript 或 Flow)
✅ JS 中常见的类型检测方式(运行时):
方法 | 说明 | 示例 |
---|---|---|
typeof | 检测基本类型(但不完全准确,比如 |
|
instanceof | 检测对象是否是某个构造函数的实例 |
|
=== / == | 比较值(== 会类型转换,=== 不会) |
|
Object.prototype.toString.call() | 更准确的类型判断 |
|
13.JS的类型检测和TS的区别
1.给一个泛型的定义让我说明它的作用
export interface Serviceidentifier<T>{
(...args:any[]):void;
type:T;
}
这个接口似乎想要描述一个 既可以像函数一样被调用(比如作为某个服务或回调),又带有 type 属性(可能用于标识这个函数的类型、类别或用途)的对象或函数类型。
但是不能直接通过编译(因为接口不能既是函数又是对象)
interface 通常用于定义对象的结构(比如 { name: string }),或者函数类型(但通常是单独定义)。如果你想定义一个既是函数、又带有额外属性的类型,通常需要使用 类型别名(
type
) 或 交叉类型(&
)**,或者将函数和属性分开定义。”1.
// 定义一个函数类型,它接受任意参数,返回 void
type ServiceFunction<T> = (...args: any[]) => void;// 然后定义一个对象类型,它包含一个 type 属性
interface ServiceWithIdentifier<T> {
type: T;
}2.
// 但函数和属性不能直接合并为一个接口,除非你使用类型合并或特殊写法
// 更常见的做法是:用一个类型,表示既有函数能力,又有 type 属性
type ServiceIdentifier<T> = {
type: T;
} & ((...args: any[]) => void);
这里用 交叉类型
&
将一个对象类型(带type: T
)和一个函数类型((...args) => void
)合并起来结果就是一个 既可以像函数一样调用,又有一个 type 属性 的类型
泛型就是“类型的占位符”,你定义一个东西时不明确说它是什么类型,而是用一个符号(比如 <T>
)来代表,等用的时候你再告诉它具体是什么类型(比如 string、number、自定义类型等)。
React.memo 的第二个参数作用是什么?如何手动控制组件更新
答案:
让你可以手动控制:什么时候认为 props “真的变了”,从而决定是否重新渲染组件。
const MyComponent = React.memo( function MyComponent({ value }) { console.log('MyComponent 被渲染了!');
return <div>Value: {value}</div>; },
(prevProps, nextProps) => {
// 只有当 value 真的变了,才重新渲染
return prevProps.value === nextProps.value; }
);
、如何手动控制组件更新?总结策略
方法 | 说明 | 适用场景 |
---|---|---|
1. 使用 React.memo(不传第二个参数) | 默认对 props 进行浅比较,避免不必要的渲染 | props 是基本类型,或引用稳定的对象 |
2. 使用 React.memo + 第二个参数(arePropsEqual) | 手动控制什么时候认为 props “真的变了”,精细控制渲染逻辑 | props 是对象/数组等引用类型,但内容未变时避免渲染 |
3. 稳定引用(useMemo / useCallback) | 配合 React.memo 使用,避免每次父组件渲染都生成新的 props 引用 | 优化对象 / 函数 类型的 props |
4. 拆分组件 / 状态下沉 | 将频繁更新的部分抽离成独立组件,减少父组件更新的影响范围 | 组件层级深、更新粒度要求高 |
React.memo
是一个高阶组件(HOC),它可以包裹一个函数组件,用来缓存(记忆)这个组件的渲染结果,避免因父组件更新而引发子组件不必要的重新渲染。
✅ 作用:只有当组件的 props
发生变化时,才会重新渲染该组件。否则复用上一次的渲染结果。
-
只比较 props,不比较 state 或 context
-
是 浅比较,不是深比较(比如对象内容变了但引用没变,也会误判)
Webpack:模块联邦(Module Federation)的实现原理及使用场景
React生命周期:对比 Class 组件与 Hooks 模拟生命周期的实现方
式
Class 组件生命周期方法 | 触发时机 | Hooks(函数组件)模拟方式 | 说明 |
---|---|---|---|
| 组件初始化时 | useState 初始化 state | 函数组件没有 constructor,state 用 |
| 组件挂载后 |
| 只在组件挂载时执行一次,相当于 componentDidMount |
| 判断是否重新渲染 | React.memo 或 useMemo / useCallback 优化 | 函数组件默认每次 state 变化都会重渲染,可通过 React.memo 或 useMemo 控制 |
| 每次更新时渲染 UI | 函数体本身就是 render | 函数组件每次返回的 JSX 就是 render |
| 组件更新并渲染到 DOM 后 |
| 在依赖项变化后执行,可以拿到最新的 state / props,模拟 componentDidUpdate |
| 组件卸载前 |
| 在组件卸载时执行,用于清理定时器、事件监听等 |
| props 变化时可能触发 | 通过 useEffect 监听 props 变化,或直接在函数参数中用 props | 函数组件中 props 是函数参数,可直接使用,一般不需要派生 state |
| DOM 更新前 | 难以完美模拟,一般很少需要 | 如果你真的需要,可能需要用 ref 手动记录信息 |
面试场次3(字节跳动三面)
-
项目难点:描述一个解决复杂问题的案例(如性能优化或架构设计)
5。React原理:解释 JSX的编译过程(如 React 17 的自动引入 jsx-3.runtime )
4.tcp和udp,直播会议用的是什么,为什么要用udp,除了建立连接耗时之外还有哪些好处
5.讲解一下美团内部使用框架的底层原理
1.项目中有没有单独封装过组件
有的,在项目的common文件下会存放项目公用组件(如:页面的头组件、页面底部组件等)项目里的feature文件下则是放项目的功能组件(轮播图、分页器、模态框这些功能组件)把这些页面重复的部分抽离出来进度单独的封装,有效减少代码量,提升了项目的开发效率。解决了传统项目中效率低、难维护、复用性低等问题。
2.在项目中发送请求怎么携带token?
当用户登录时,后端会把用户的信息和token返回给我们,我们将t用户信息和oken进行存储在状态管理库中,在axios二次封装中的请求拦截器中将token取出添加到config请求头上携带传给服务器,
3.工作中有用到git吗?
有的,在之前的公司,基本上用的都是git进行多人开发,git是一个分布式版本管理工具,首先是获取项目:先创建自己的项目文件夹,右键git bash here,访问远程仓库复制项目链接,git clone带上项目链接地址,将项目克隆到你的文件夹下就成功地拉取了项目。
然后使用 git init 命令把目录变成git可以管理的仓库,进行开发,
开发完成后,使用 git add.将文件都添加到暂存区里面去再使用 git commit -m'提交说明' 把文件提交到本地仓库先使用 git pull 拉取远程仓库代码,避免远程仓库与本地代码不一致时发生冲突再 git push 把本地仓库的代码提交到远程仓库