:手写进阶!字节高频手写难题,搞定直接冲二面)
前言继上一篇吃透基础手写题防抖、节流、深拷贝、Promise后这一篇咱们升级难度 ——字节一面进阶手写题字节的手写题从来不是 “会写就行”而是 “写得严谨、懂原理、能拓展”。比如 call/apply/bind不只是让你实现功能还要你说清区别Promise 进阶要你实现链式调用、Promise.all这些都是字节一面后半段、二面前半段的高频考点。很多人栽在进阶手写题上不是不会而是没吃透底层逻辑比如 bind 的柯里化特性、Promise.all 的失败快速返回。这一篇我依旧拿字节真实面试原题用最接地气的语言把每道题的原理、代码、细节、追问都讲透让你写出来的代码直接戳中面试官的加分点不用死记硬背理解后就能默写。真题 1手写 call/apply/bind —— 字节高频三者成对考面试原题请分别手写 call、apply、bind 方法要求实现改变 this 指向的核心功能说明三者的区别解释 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)否返回新函数字节高频追问必背bind 为什么会返回一个新函数答因为 bind 的核心是 “绑定 this 指向”但不立即执行函数返回新函数后后续可以根据需要多次调用符合实际业务场景比如绑定事件回调避免 this 指向错误。bind 的柯里化特性是什么答柯里化就是 “将多参数函数拆分成多个单参数 / 少参数函数”bind 支持分多次传递参数第一次传 this 和部分参数后续调用新函数时再传剩余参数最终合并所有参数执行。用 new 调用 bind 返回的新函数this 会指向谁答会指向 new 创建的实例而不是 bind 绑定的 context—— 这是字节面试官常考的细节也是我手写代码中判断this instanceof boundFn的原因。真题 2手写函数柯里化 —— 字节进阶结合 bind 考面试原题请手写一个通用的柯里化函数要求支持分多次传递参数直到参数传完才执行举例说明应用场景结合字节业务说明说明柯里化的核心作用核心考察点闭包的深度应用、函数参数收集、函数执行控制字节常和 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字节业务应用场景面试必说加分表单参数收集字节后台管理系统、抖音注册表单分步骤收集用户输入的参数全部收集完成后再发起提交请求避免参数缺失。接口请求封装字节接口开发提前固定部分参数比如接口地址、请求方式后续只需要传递变化的参数比如请求参数简化接口调用。函数复用抖音组件开发将常用函数柯里化固定部分参数生成新的复用函数减少重复代码。字节高频追问必背柯里化的核心作用是什么答① 分步骤收集参数提升代码灵活性② 复用函数固定部分参数减少重复代码③ 延迟执行直到参数齐全才执行函数。柯里化和 bind 的关系是什么答bind 的柯里化特性本质就是柯里化的应用 ——bind 先固定 this 和部分参数返回新函数后续调用新函数时再传递剩余参数和柯里化 “分步骤收集参数” 的核心逻辑一致。如何实现一个支持不定参数的柯里化函数进阶追问答不用判断参数个数而是当调用函数时不传参数就执行目标函数比如 curriedAdd (1)(2)(3)() 6修改代码即可实现下文可补充。真题 3Promise 进阶手写链式调用 Promise.all—— 字节重中之重面试原题请完善上一篇的 Promise要求实现 then 方法的链式调用支持 .then ().then ()手写 Promise.all 方法说明 Promise.all 的失败机制核心考察点Promise 底层原理、异步回调的链式处理、Promise 并发控制字节二面高频难度中等通俗解析Promise 链式调用到底怎么实现上一篇我们写的 Promisethen 方法只能调用一次不能链式调用 —— 而真实的 Promise之所以能 .then ().then ()核心是「每次调用 then都返回一个新的 Promise」上一个 then 的返回值会作为下一个 then 的参数。就像 “接力赛”第一棒跑完上一个 then 执行完成把接力棒返回值交给第二棒下一个 then第二棒跑完再交给第三棒直到所有接力棒跑完所有 then 执行完成。而 Promise.all就像 “团队比赛”多个选手Promise同时出发只要有一个选手失败reject整个团队就失败只有所有选手都成功resolve团队才成功最终返回所有选手的结果。1. 完善 Promise实现 then 链式调用jsconst 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毫秒后输出 失败快速失败 );字节高频追问必背Promise.then 为什么能链式调用答因为每次调用 then 方法都会返回一个新的 Promise上一个 then 的回调返回值会作为下一个 then 的参数如果返回值是 Promise下一个 then 会等待这个 Promise 执行完成再执行回调。Promise.all 的失败机制是什么答“快速失败” 机制 —— 只要有一个 Promise 被 rejectPromise.all 会立即 reject 这个错误不再等待其他 Promise 执行这是字节面试官看重的核心特性。Promise.all 和 Promise.race 的区别进阶追问答Promise.all 是 “所有成功才成功一个失败就失败”Promise.race 是 “谁先执行完成无论成功还是失败就返回谁的结果”字节业务中常用 Promise.race 实现请求超时控制。本篇总结字节面试直接背字节一面进阶手写题核心就这 3 类考频排序Promise 进阶 call/apply/bind 柯里化记住这 3 个关键点call/apply/bind 核心是「改变 this 指向」bind 多了 “柯里化 返回新函数”一定要处理原型继承和 new 调用的场景柯里化核心是「分步骤收集参数」通用版要判断参数个数结合字节业务场景说明更加分Promise 链式调用的核心是「每次 then 返回新 Promise」Promise.all 核心是「并发控制 快速失败」细节要处理异步回调和结果顺序。这一篇的手写题你能默写下来字节一面的手写关基本满分直接有底气冲二面