当前位置: 首页 > news >正文

字节前端面试真题解析系列(第三篇):手写进阶!字节高频手写难题,搞定直接冲二面

前言

继上一篇吃透基础手写题(防抖、节流、深拷贝、Promise)后,这一篇咱们升级难度 ——字节一面进阶手写题

字节的手写题,从来不是 “会写就行”,而是 “写得严谨、懂原理、能拓展”。比如 call/apply/bind,不只是让你实现功能,还要你说清区别;Promise 进阶,要你实现链式调用、Promise.all,这些都是字节一面后半段、二面前半段的高频考点。

很多人栽在进阶手写题上,不是不会,而是没吃透底层逻辑(比如 bind 的柯里化特性、Promise.all 的失败快速返回)。这一篇,我依旧拿字节真实面试原题,用最接地气的语言,把每道题的原理、代码、细节、追问都讲透,让你写出来的代码,直接戳中面试官的加分点,不用死记硬背,理解后就能默写。


真题 1:手写 call/apply/bind —— 字节高频,三者成对考

面试原题

请分别手写 call、apply、bind 方法,要求:

  1. 实现改变 this 指向的核心功能
  2. 说明三者的区别
  3. 解释 bind 为什么会返回一个新函数,以及它的柯里化特性

核心考察点

this 指向的手动控制、函数参数传递、闭包的应用、柯里化思想(字节超爱抠 bind 的细节)

通俗解析(先懂核心,再写代码)

call、apply、bind 的核心作用,都是「手动改变函数的 this 指向」,就像 “借调”—— 比如函数 A 本来指向对象 X,通过这三个方法,能让它临时指向对象 Y,执行 Y 的 “任务”。

举个生动的例子:你(函数)本来在自己家(this 指向自己),call/apply 就像 “临时去朋友家帮忙”(立即执行,帮完就回自己家);bind 就像 “把朋友家的钥匙拿过来,以后想什么时候去帮忙就什么时候去”(不立即执行,返回一个新函数,后续可多次调用)。

1. 手写 call 方法(最基础,字节入门手写)

js

// 给 Function 原型添加 myCall 方法(所有函数都能调用) Function.prototype.myCall = function (context) { // 1. 处理 context:如果没传,默认指向 window(非严格模式) context = context || window; // 2. 把当前函数(this)挂载到 context 上,作为它的一个属性(比如 context.fn = 当前函数) // 用 Symbol 避免属性名冲突(字节细节加分点) const fn = Symbol('fn'); context[fn] = this; // 3. 处理参数:call 是单个传参,从第二个参数开始,获取所有参数 const args = Array.from(arguments).slice(1); // 4. 执行函数,拿到返回值(此时函数的 this 指向 context) const result = context[fn](...args); // 5. 删除挂载的函数,避免污染 context(字节关键细节) delete context[fn]; // 6. 返回函数执行结果(保持原生 call 的特性) return result; };

2. 手写 apply 方法(和 call 几乎一样,区别在参数)

js

// 给 Function 原型添加 myApply 方法 Function.prototype.myApply = function (context) { context = context || window; const fn = Symbol('fn'); context[fn] = this; // 关键区别:apply 第二个参数是数组/类数组,没有就传空数组 let result; if (arguments[1]) { result = context[fn](...arguments[1]); } else { result = context[fn](); } delete context[fn]; return result; };

3. 手写 bind 方法(字节重点,难在柯里化和 this 绑定)

js

// 给 Function 原型添加 myBind 方法 Function.prototype.myBind = function (context) { const self = this; // 保存当前函数(this),避免闭包中 this 指向改变 // 1. 处理参数:bind 支持柯里化,先获取第一次传入的参数(从第二个开始) const args1 = Array.from(arguments).slice(1); // 2. 返回一个新函数(bind 不立即执行,这是和 call/apply 的核心区别) const boundFn = function () { // 3. 处理第二次传入的参数(柯里化特性) const args2 = Array.from(arguments); // 4. 合并两次参数,执行函数 // 关键:判断当前 this 是不是 boundFn 的实例(如果用 new 调用,this 指向实例) return self.apply( this instanceof boundFn ? this : context, args1.concat(args2) ); }; // 5. 继承原函数的原型(字节加分细节,保证 new 调用时的原型链正确) boundFn.prototype = Object.create(self.prototype); boundFn.prototype.constructor = boundFn; return boundFn; };

三者核心区别(字节必问,一句话记死)

表格

方法核心区别参数传递是否立即执行
call单个传参逗号分隔(fn.call (ctx, 1, 2, 3))
apply数组传参数组 / 类数组(fn.apply (ctx, [1,2,3]))
bind柯里化传参分多次传参(fn.bind (ctx,1)(2,3))否(返回新函数)

字节高频追问(必背)

  1. bind 为什么会返回一个新函数?答:因为 bind 的核心是 “绑定 this 指向”,但不立即执行函数,返回新函数后,后续可以根据需要多次调用,符合实际业务场景(比如绑定事件回调,避免 this 指向错误)。
  2. bind 的柯里化特性是什么?答:柯里化就是 “将多参数函数,拆分成多个单参数 / 少参数函数”,bind 支持分多次传递参数,第一次传 this 和部分参数,后续调用新函数时,再传剩余参数,最终合并所有参数执行。
  3. 用 new 调用 bind 返回的新函数,this 会指向谁?答:会指向 new 创建的实例,而不是 bind 绑定的 context—— 这是字节面试官常考的细节,也是我手写代码中判断this instanceof boundFn的原因。

真题 2:手写函数柯里化 —— 字节进阶,结合 bind 考

面试原题

请手写一个通用的柯里化函数,要求:

  1. 支持分多次传递参数,直到参数传完才执行
  2. 举例说明应用场景,结合字节业务说明
  3. 说明柯里化的核心作用

核心考察点

闭包的深度应用、函数参数收集、函数执行控制(字节常和 bind、数组方法结合考)

通俗解析(柯里化到底是什么?)

柯里化就像「拼乐高」:一个需要多个零件(参数)的乐高模型,你不用一次性把所有零件都凑齐,而是先拼一部分,再拼一部分,直到所有零件都凑齐,才完成整个模型(执行函数)。

比如 add (1)(2)(3) = 6,就是最经典的柯里化 —— 分三次传递参数,每次传递一个,直到传递完 3 个参数,才执行加法操作。字节业务中,很多场景需要 “分步骤收集参数”,比如表单提交(先收集用户名,再收集密码,最后收集验证码,全部收集完再提交)。

字节标准手写代码(通用版,可直接复用)

js

// 通用柯里化函数:fn=目标函数,args=初始参数(可选) function currying(fn, ...args) { // 核心:判断当前收集的参数个数,是否等于目标函数的形参个数 const needArgs = fn.length; // 目标函数需要的参数个数 const currentArgs = args; // 已收集的参数(闭包保存) // 返回一个新函数,用于继续收集参数 return function (...newArgs) { const allArgs = currentArgs.concat(newArgs); // 合并已收集和新传入的参数 // 1. 如果参数够了,执行目标函数 if (allArgs.length >= needArgs) { return fn.apply(this, allArgs); } // 2. 如果参数不够,继续柯里化,返回新函数 else { return currying(fn, ...allArgs); } }; }

测试代码(面试时主动写,加分)

js

// 测试1:简单加法柯里化 const add = (a, b, c) => a + b + c; const curriedAdd = currying(add); console.log(curriedAdd(1)(2)(3)); // 6(分3次传参) console.log(curriedAdd(1, 2)(3)); // 6(分2次传参) // 测试2:字节业务场景(表单参数收集) const submitForm = (username, password, code) => { console.log(`提交表单:用户名${username},密码${password},验证码${code}`); }; const curriedSubmit = currying(submitForm); curriedSubmit('zhangsan')('123456')('666666'); // 提交表单:用户名zhangsan,密码123456,验证码666666

字节业务应用场景(面试必说,加分)

  • 表单参数收集(字节后台管理系统、抖音注册表单):分步骤收集用户输入的参数,全部收集完成后,再发起提交请求,避免参数缺失。
  • 接口请求封装(字节接口开发):提前固定部分参数(比如接口地址、请求方式),后续只需要传递变化的参数(比如请求参数),简化接口调用。
  • 函数复用(抖音组件开发):将常用函数柯里化,固定部分参数,生成新的复用函数,减少重复代码。

字节高频追问(必背)

  1. 柯里化的核心作用是什么?答:① 分步骤收集参数,提升代码灵活性;② 复用函数,固定部分参数,减少重复代码;③ 延迟执行,直到参数齐全才执行函数。
  2. 柯里化和 bind 的关系是什么?答:bind 的柯里化特性,本质就是柯里化的应用 ——bind 先固定 this 和部分参数,返回新函数,后续调用新函数时,再传递剩余参数,和柯里化 “分步骤收集参数” 的核心逻辑一致。
  3. 如何实现一个支持不定参数的柯里化函数?(进阶追问)答:不用判断参数个数,而是当调用函数时不传参数,就执行目标函数(比如 curriedAdd (1)(2)(3)() = 6),修改代码即可实现(下文可补充)。

真题 3:Promise 进阶手写(链式调用 + Promise.all)—— 字节重中之重

面试原题

请完善上一篇的 Promise,要求:

  1. 实现 then 方法的链式调用(支持 .then ().then ())
  2. 手写 Promise.all 方法
  3. 说明 Promise.all 的失败机制

核心考察点

Promise 底层原理、异步回调的链式处理、Promise 并发控制(字节二面高频,难度中等)

通俗解析(Promise 链式调用到底怎么实现?)

上一篇我们写的 Promise,then 方法只能调用一次,不能链式调用 —— 而真实的 Promise,之所以能 .then ().then (),核心是「每次调用 then,都返回一个新的 Promise」,上一个 then 的返回值,会作为下一个 then 的参数。

就像 “接力赛”:第一棒跑完(上一个 then 执行完成),把接力棒(返回值)交给第二棒(下一个 then),第二棒跑完,再交给第三棒,直到所有接力棒跑完(所有 then 执行完成)。

而 Promise.all,就像 “团队比赛”:多个选手(Promise)同时出发,只要有一个选手失败(reject),整个团队就失败;只有所有选手都成功(resolve),团队才成功,最终返回所有选手的结果。

1. 完善 Promise,实现 then 链式调用

js

const PENDING = 'pending'; const FULFILLED = 'fulfilled'; const REJECTED = 'rejected'; class MyPromise { constructor(executor) { this.status = PENDING; this.value = undefined; this.reason = undefined; // 新增:保存 then 回调(处理异步 resolve/reject) this.onFulfilledCallbacks = []; this.onRejectedCallbacks = []; const resolve = (value) => { if (this.status !== PENDING) return; this.status = FULFILLED; this.value = value; // 异步 resolve 时,执行所有成功回调 this.onFulfilledCallbacks.forEach((cb) => cb()); }; const reject = (reason) => { if (this.status !== PENDING) return; this.status = REJECTED; this.reason = reason; // 异步 reject 时,执行所有失败回调 this.onRejectedCallbacks.forEach((cb) => cb()); }; try { executor(resolve, reject); } catch (err) { reject(err); } } // 完善 then 方法,实现链式调用 then(onFulfilled, onRejected) { // 处理默认回调(如果没传,就透传值/错误) onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (value) => value; onRejected = typeof onRejected === 'function' ? onRejected : (reason) => { throw reason; }; // 核心:每次 then 都返回一个新的 Promise,实现链式调用 const newPromise = new MyPromise((resolve, reject) => { // 成功状态:异步执行回调(保证 then 回调异步执行,符合原生特性) if (this.status === FULFILLED) { setTimeout(() => { try { // 执行上一个 then 的成功回调,拿到返回值 const result = onFulfilled(this.value); // 判断返回值是不是 Promise,如果是,等待它执行完成 if (result instanceof MyPromise) { result.then(resolve, reject); } else { // 不是 Promise,直接 resolve 返回值 resolve(result); } } catch (err) { // 回调执行出错,reject 错误 reject(err); } }, 0); } // 失败状态:和成功状态逻辑一致 if (this.status === REJECTED) { setTimeout(() => { try { const result = onRejected(this.reason); if (result instanceof MyPromise) { result.then(resolve, reject); } else { resolve(result); // 失败回调返回非 Promise,依旧 resolve(原生特性) } } catch (err) { reject(err); } }, 0); } // 等待状态:将回调保存起来,等 resolve/reject 时执行 if (this.status === PENDING) { this.onFulfilledCallbacks.push(() => { setTimeout(() => { try { const result = onFulfilled(this.value); if (result instanceof MyPromise) { result.then(resolve, reject); } else { resolve(result); } } catch (err) { reject(err); } }, 0); }); this.onRejectedCallbacks.push(() => { setTimeout(() => { try { const result = onRejected(this.reason); if (result instanceof MyPromise) { result.then(resolve, reject); } else { resolve(result); } } catch (err) { reject(err); } }, 0); }); } }); return newPromise; // 返回新 Promise,实现链式调用 } }

2. 手写 Promise.all 方法(字节高频,核心是并发控制)

js

// 给 MyPromise 新增 all 静态方法 MyPromise.all = function (promises) { return new MyPromise((resolve, reject) => { // 1. 处理参数:如果不是数组,直接 reject if (!Array.isArray(promises)) { return reject(new TypeError('Promise.all 接收的参数必须是数组')); } const result = []; // 保存所有 Promise 的成功结果 let count = 0; // 记录已完成的 Promise 个数 // 2. 如果数组为空,直接 resolve 空数组 if (promises.length === 0) { return resolve(result); } // 3. 遍历每个 Promise,执行并收集结果 promises.forEach((p, index) => { // 用 then 处理每个 Promise,无论传入的是 Promise 还是普通值 MyPromise.resolve(p).then( (res) => { result[index] = res; // 按顺序保存结果(字节细节,保证结果顺序和传入顺序一致) count++; // 所有 Promise 都成功,才 resolve 结果数组 if (count === promises.length) { resolve(result); } }, (err) => { // 核心:只要有一个 Promise 失败,立即 reject 错误(失败快速返回) reject(err); } ); }); }); }; // 新增 resolve 静态方法(处理普通值转 Promise) MyPromise.resolve = function (value) { if (value instanceof MyPromise) { return value; // 如果是 Promise,直接返回 } // 如果是普通值,返回一个 resolved 状态的 Promise return new MyPromise((resolve) => resolve(value)); };

测试代码(面试时主动写,加分)

js

// 测试 then 链式调用 const p1 = new MyPromise((resolve) => { setTimeout(() => resolve(1), 1000); }); p1.then((res) => { console.log(res); // 1秒后输出 1 return res + 1; }).then((res) => { console.log(res); // 输出 2 }); // 测试 Promise.all const p2 = new MyPromise((resolve) => setTimeout(() => resolve(2), 500)); const p3 = new MyPromise((resolve) => setTimeout(() => resolve(3), 1000)); MyPromise.all([p2, p3]).then((res) => { console.log(res); // 1秒后输出 [2, 3](按传入顺序) }); // 测试 Promise.all 失败 const p4 = new MyPromise((resolve, reject) => setTimeout(() => reject('失败'), 300)); MyPromise.all([p2, p4, p3]).then( (res) => console.log(res), (err) => console.log(err) // 300毫秒后输出 失败(快速失败) );

字节高频追问(必背)

  1. Promise.then 为什么能链式调用?答:因为每次调用 then 方法,都会返回一个新的 Promise,上一个 then 的回调返回值,会作为下一个 then 的参数;如果返回值是 Promise,下一个 then 会等待这个 Promise 执行完成,再执行回调。
  2. Promise.all 的失败机制是什么?答:“快速失败” 机制 —— 只要有一个 Promise 被 reject,Promise.all 会立即 reject 这个错误,不再等待其他 Promise 执行,这是字节面试官看重的核心特性。
  3. Promise.all 和 Promise.race 的区别?(进阶追问)答:Promise.all 是 “所有成功才成功,一个失败就失败”;Promise.race 是 “谁先执行完成(无论成功还是失败),就返回谁的结果”,字节业务中,常用 Promise.race 实现请求超时控制。

本篇总结(字节面试直接背)

字节一面进阶手写题,核心就这 3 类,考频排序:Promise 进阶 > call/apply/bind > 柯里化,记住这 3 个关键点:

  1. call/apply/bind 核心是「改变 this 指向」,bind 多了 “柯里化 + 返回新函数”,一定要处理原型继承和 new 调用的场景;
  2. 柯里化核心是「分步骤收集参数」,通用版要判断参数个数,结合字节业务场景说明更加分;
  3. Promise 链式调用的核心是「每次 then 返回新 Promise」,Promise.all 核心是「并发控制 + 快速失败」,细节要处理异步回调和结果顺序。

这一篇的手写题,你能默写下来,字节一面的手写关基本满分,直接有底气冲二面!

http://www.cnnetsun.cn/news/1359990.html

相关文章:

  • Maxwell电机多目标尺寸优化案例:使用Ansys Maxwell与OptiSlang的永磁...
  • 某雷赛86闭环步进驱动方案 HBS86H 86闭环电机驱动器/混合伺服驱动器。 原理图+PCB...
  • 基于java+ssm+vue的线上编程学习系统989
  • FX3U-IE-V12.2 PLC源代码解析及网口实现本地或远程穿透编程、监控
  • 工商业储能项目验收被拒?这些仪表选型“硬指标”提前对好
  • 西门子S7系列PLC C#上位机通信系统功能说明文档
  • 如何看待AI Agent(智能体)的伪效率问题
  • 常用的office word vba宏
  • Ubuntu 服务器之间互传文件夹
  • 人力资源战略与业务战略对齐的重要性及正确实施方法
  • 影视仓2026最新接口配置合集,tvbox4K高清源,值得收藏!
  • ArkClaw vs KimiClaw vs MaxClaw:个人用户实际体验对比
  • Setapp 3.44.1:macOS第三方应用商店功能介绍与使用体验
  • 基于定时器的按键计时操作
  • 科研党收藏!更贴合论文写作全流程的降AIGC网站,千笔·专业降AI率智能体 VS WPS AI
  • 动态住宅IP在跨境业务中的技术价值与实践指南
  • AF555 α-银环蛇D素,AF555-α-BTX荧光标记的光谱特性
  • Git误操作急救手册大纲,一招在手走遍天下
  • 被查出AI率不要慌!2026免费毕业论文去痕神器盘点
  • 【JAVA 运算】
  • 消息中间件RabbitMQ04:路由模式+死信队列的应用实践模板
  • 腾讯面完了,AI Agent真的好颠
  • RK3588 OpenClaw 定时任务踩坑与守护进程方案
  • 金仓数据库 SQL 防火墙实战:内核层防注入,配置 / 调优 / 审计全代码解析
  • 5 万行 Rust 代码重塑 AI Agent:SkillLite 从 Python 到 Rust 的架构演进与选型复盘
  • 第4篇:中文语言 华夏本源语言——极简语法大全
  • 什么是偶极矩?
  • java基础学习3(数据类型转换、运算符)
  • CentOS Stream 8配置DNS
  • PrivescCheck与渗透测试:如何集成到红队工作流程中