手写 Function.prototype.call 与 Function.prototype.apply 面试题满分答案

1. 核心思路(一句话)

利用"对象属性调用隐式绑定 this"的原理,通过 Symbol 创建唯一临时属性避免污染,结合 try-finally 确保异常安全清理,并严格处理 null/undefined 及原始值包装等边界条件。


2. 问题矛盾分析

矛盾类型 具体表现 解决策略
主要矛盾 如何在不使用原生 API 的前提下改变函数执行时的 this 指向 将函数作为目标对象的临时属性进行调用(隐式绑定)
次要矛盾1 临时属性名可能与对象原有属性冲突(属性污染) 使用 Symbol() 生成全局唯一的键名
次要矛盾2 函数执行报错导致临时属性无法删除(内存泄漏/状态污染) 使用 try...finally 结构保证清理逻辑必执行
次要矛盾3 this 传入 null/undefined 或原始值时的规范对齐 null/undefined 转全局对象;原始值用 Object() 包装为对象
次要矛盾4 callapply 参数形式差异 call 用剩余参数收集;apply 校验数组并用扩展运算符展开

3. 解决方案流程图(文本版)

text 复制代码
[开始: myCall/myApply]
       │
       ▼
[1. this 边界处理]
   ├─ null / undefined → globalThis (浏览器window/Node global)
   └─ 原始值(number/string/boolean) → Object() 包装为对象
       │
       ▼
[2. 创建唯一临时属性键]
   └─ const fnKey = Symbol()  ← 防止属性名冲突
       │
       ▼
[3. 挂载函数到目标对象]
   └─ context[fnKey] = this (当前要执行的函数)
       │
       ▼
[4. try { 执行函数 } ]
   ├─ call: context[fnKey](...args)
   └─ apply: context[fnKey](...(Array.isArray(args)?args:[]))
       │
       ▼
[5. finally { 清理 }]
   └─ delete context[fnKey]  ← 无论成功/报错都执行
       │
       ▼
[6. 返回执行结果]

4. 完整示例代码(含详细注释)

4.1 工业级 myCall 实现
javascript 复制代码
/**
 * 手写 Function.prototype.call
 * 核心原理:利用 obj.fn() 调用时 this 自动绑定到 obj 的特性
 */
Function.prototype.myCall = function (context, ...args) {
  // ========== 1. 边界处理:this 指向 ==========
  // 规范:null/undefined 在非严格模式下指向全局对象
  // 使用 globalThis 兼容浏览器(window)、Node(global)、WebWorker(self)
  if (context === null || context === undefined) {
    context = globalThis;
  } else {
    // 规范:原始值(数字/字符串/布尔)需要被包装为对应的对象
    // Object(123) → Number对象,这样才能在其上添加临时属性
    context = Object(context);
  }

  // ========== 2. 防止属性污染 ==========
  // 使用 Symbol 创建全局唯一的属性键
  // 避免与 context 上已有的同名方法冲突(如 context.fn 已存在)
  const fnKey = Symbol('tempFn');

  // 将要执行的函数(即调用 myCall 的那个函数)挂载为临时属性
  context[fnKey] = this;

  // ========== 3. 执行函数并确保清理 ==========
  let result;
  try {
    // 通过对象方法调用触发隐式 this 绑定
    // ...args 将剩余参数展开传递给原函数
    result = context[fnKey](...args);
  } finally {
    // 【关键】finally 块无论 try 中是否抛错都会执行
    // 确保临时属性一定被删除,避免内存泄漏和对象污染
    delete context[fnKey];
  }

  // ========== 4. 返回原函数执行结果 ==========
  return result;
};
4.2 工业级 myApply 实现
javascript 复制代码
/**
 * 手写 Function.prototype.apply
 * 与 myCall 的唯一区别:第二个参数是数组(或类数组),需展开传递
 */
Function.prototype.myApply = function (context, argsArray) {
  // ========== 1. this 边界处理(与 myCall 完全相同)==========
  if (context === null || context === undefined) {
    context = globalThis;
  } else {
    context = Object(context);
  }

  // ========== 2. 参数安全校验 ==========
  // 规范:apply 第二个参数如果不是数组/类数组,应视为空参数列表
  // 这里做防御性处理,避免非数组值导致展开运算符报错
  const safeArgs = Array.isArray(argsArray) ? argsArray : [];

  // ========== 3. Symbol + try-finally(与 myCall 完全相同)==========
  const fnKey = Symbol('tempFn');
  context[fnKey] = this;

  let result;
  try {
    // 使用扩展运算符将数组展开为参数列表
    result = context[fnKey](...safeArgs);
  } finally {
    delete context[fnKey];
  }

  return result;
};
4.3 测试用例(验证边界场景)
javascript 复制代码
// ✅ 测试1:基本上下文切换
const obj = { name: 'Alice' };
function greet(greeting) {
  return `${greeting}, ${this.name}`;
}
console.log(greet.myCall(obj, 'Hello'));      // "Hello, Alice"
console.log(greet.myApply(obj, ['Hi']));      // "Hi, Alice"

// ✅ 测试2:null/undefined → 全局对象
function getGlobal() { return this; }
console.log(getGlobal.myCall(null) === globalThis);   // true
console.log(getGlobal.myApply(undefined) === globalThis); // true

// ✅ 测试3:原始值 this 被正确包装
function getType() { return typeof this; }
console.log(getType.myCall(123));             // "object" (Number包装)
console.log(getType.myCall('hello'));         // "object" (String包装)

// ✅ 测试4:属性不污染
const target = { existingProp: 'original' };
function noop() {}
noop.myCall(target);
console.log(target.existingProp);             // "original" (未被覆盖)
console.log(Object.getOwnPropertySymbols(target).length); // 0 (Symbol已清理)

// ✅ 测试5:函数抛错时仍清理临时属性
function thrower() { throw new Error('boom'); }
try { thrower.myCall(obj); } catch(e) {}
console.log(Object.getOwnPropertySymbols(obj).length); // 0 (异常后仍清理)

// ✅ 测试6:apply 传入非数组不报错
function sum(a, b) { return (a||0) + (b||0); }
console.log(sum.myApply(null, null));          // 0
console.log(sum.myApply(null, 'not-array'));   // 0

5. 使用场景与边界场景补充

场景分类 具体场景 说明
使用场景 借用其他对象的方法 Array.prototype.slice.myCall(arguments) 将类数组转数组
构造函数继承 Parent.call(this, ...args) 在子类中调用父类构造器
动态参数调用 Math.max.myApply(null, arr) 求数组最大值
边界场景 严格模式下的 null/undefined ⚠️ 注意 :原生 call 在严格模式下 this 就是 null 本身,不会转全局对象。上述实现按非严格模式处理,面试时需主动说明此差异
Symbol 不可用环境 IE11 及以下不支持 Symbol,降级方案:用 '__temp_' + Date.now() + Math.random() 生成伪唯一键
冻结对象 Object.freeze() 冻结对象无法添加属性,myCall 会抛 TypeError。原生 call 同样如此,行为一致
apply 传入类数组 原生 apply 支持类数组(如 arguments),上述实现仅校验 Array.isArray。如需完整兼容,应改用 Array.from(argsArray) 或手动遍历

6. 更好的优化方案(加分项)

Reflect.apply 方案(现代环境推荐)

如果面试官允许使用 ES6+ API,可以用 Reflect.apply 一行实现,且天然规避属性污染问题:

javascript 复制代码
// Reflect.apply 直接指定 this 和参数,无需临时属性
Function.prototype.myCall = function (context, ...args) {
  // Reflect.apply 内部机制不涉及属性赋值,零污染风险
  // 但仍需处理 null/undefined 和原始值的边界
  const safeContext = (context === null || context === undefined)
    ? globalThis
    : Object(context);
  return Reflect.apply(this, safeContext, args);
};

优势 :无临时属性、无 Symbol 依赖、无 try-finally 开销、语义更清晰。

劣势 :IE 不支持;面试中通常要求不用原生 call/apply/reflect,所以此方案作为知识广度展示即可。


🏆 满分答案总结(面试口述版)

手写 call/apply 的核心考察点是 this 绑定原理 + 边界防御 + 工程严谨性。我的实现分四步:

  1. this 规范化null/undefinedglobalThis,原始值用 Object() 包装,对齐 ECMAScript 规范;
  2. 防属性污染 :用 Symbol() 创建唯一临时属性键,杜绝与目标对象已有属性冲突;
  3. 异常安全清理 :用 try...finally 包裹执行逻辑,确保即使函数抛错,临时属性也一定被 delete,避免内存泄漏;
  4. 参数差异化处理call 用剩余参数收集,apply 校验数组合法性后用扩展运算符展开。

此外我还注意到两个易忽略点:严格模式下 null 不转全局对象 的差异需在代码注释中标注;以及 apply 原生支持类数组 ,完整实现应用 Array.from 兼容。如果环境允许,Reflect.apply 是最优解,因为它从根本上消除了临时属性的副作用。

相关推荐
Zenova EdgeOS1 小时前
C++ 工业边缘 libcurl HTTP 客户端实战
开发语言·c++·网络协议·边缘计算
Jackson_GJH2 小时前
Qt 右键自定义菜单的实现
开发语言·c++·qt
CoderYanger2 小时前
前端基础——JavaScript(WebAPI)(下篇)
java·开发语言·前端·javascript·程序人生·面试·职场和发展
小木_.3 小时前
Python 离线识别滑块缺口距离,项目推荐
开发语言·python·滑块识别·人机验证·滑块缺口·缺口识别
liliangcsdn3 小时前
因子权重矩阵处理-因子权重收缩Shrinkage算法的探索
开发语言·python·机器学习
JunjunZ4 小时前
Naive UI 虚拟级联选择器适配 Element Plus 风格
前端·javascript·vue.js
aramae4 小时前
模拟实现strstr函数(C语言)
c语言·开发语言·算法
liliangcsdn4 小时前
因子权重矩阵处理-滞回缓冲带+降频稳定化动态重选
开发语言·python·算法
ShuiShenHuoLe4 小时前
golang-jwt v5 入门
开发语言·后端·golang