手写 call、apply、bind 的实现

Call的实现

基本实现思路:
  1. 将函数设置为对象的属性
  2. 执行该函数
  3. 删除该属性
  4. 返回执行结果
javascript 复制代码
Function.prototype.myCall = function (context, ...args) {
  // 如果 context 为 null 或者 undefined, 则指向全局对象 (浏览器中是window)
  context = context || globalThis;

  // 防止覆盖原有属性,使用 Symbol 作为唯一键
  const fnKey = Symbol("fn");

  // 将当前函数作为 context 的方法
  context[fnKey] = this;

  // 执行函数
  const result = context[fnKey](...args);

  // 删除临时添加的方法
  delete context[fnKey];

  return result;
};

// 更严谨的版本 (考虑更多边界情况)
Function.prototype.myCall = function (context = globalThis, ...args) {
  // 确保 context 是对象(如果不是,转换为对象包装)
  if (context === null || context === undefined) {
    context = globalThis;
  } else {
    context = Object(context);
  }

  // 使用唯一键
  const fnKey = Symbol("fn");

  // 将当前函数绑定到context
  context[fnKey] = this;

  try {
    // 执行函数
    return context[fnKey](...args);
  } finally {
    // 确保删除临时属性
    delete context[fnKey];
  }
};

apply的实现

applycall类似,只是第二个参数是数组

javascript 复制代码
Function.prototype.myApply = function (context, argsArray) {
  // 处理 context
  context = context || globalThis;

  // 如果argsArray不是数组或者类数组,则当作空数组处理
  if (
    !Array.isArray(argsArray) &&
    !(argsArray && typeof argsArray === "object" && "length" in argsArray)
  ) {
    argsArray = [];
  }

  // 创建唯一键
  const fnKey = Symbol("fn");

  // 将函数绑定到 context
  context[fnKey] = this;

  try {
    // 执行函数,使用展开运算符传递参数
    const result = context[fnKey](...argsArray);
    return result;
  } finally {
    // 清理
    delete context[fnKey];
  }
};

// 更简洁的版本(基于myCall)
Function.prototype.myApply = function (context, argsArray) {
  context = context || globalThis;
  argsArray = argsArray || [];

  // 使用 myCall 的实现
  const fnKey = Symbol("fn");
  context[fnKey] = this;

  try {
    return context[fnKey](...argsArray);
  } finally {
    delete context[fnKey];
  }
};

bind的实现

bind返回一个新函数,需要处理更多边界情况

javascript 复制代码
Function.prototype.myBind = function (context, ...bindArgs) {
  // 保存原函数
  const originalFunc = this;

  // 确保 context 是对象
  context = context || globalThis;

  // 返回的绑定函数
  const boundFunc = function (...callArgs) {
    // 判断是否通过 new 调用
    const isNewCall = this instanceof boundFunc;

    // 如果是 new 调用,this指向新创建的对象,而不是 context
    const thisContext = isNewCall ? this : context;

    // 合并参数
    const allArgs = bindArgs.concat(callArgs);

    // 执行原函数
    return originalFunc.apply(thisContext, allArgs);
  };

  // 维护原型关系 (为了支持 new 操作)
  // 使用一个空函数作为中介,避免直接修改boundFunc.prototype影响originalFunc.prototype
  const TempFunc = function () {};
  TempFunc.prototype = originalFunc.prototype;
  boundFunc.prototype = new TempFunc();

  return boundFunc;
};

更完整的 bind 实现(支持更多特性)

javascript 复制代码
Function.prototype.myBind = function (context, ...bindArgs) {
  // 保存原函数
  const originalFunc = this;

  // 判断是否是构造函数
  if (typeof originalFunc !== "function") {
    throw new TypeError("Function.prototype.bind called on non-function");
  }

  // 返回的绑定函数
  const boundFunc = function (...callArgs) {
    // 判断是否通过 new 调用
    // 通过 new 调用时,this应该是 boundFunc 的实例
    const isNewCall = this instanceof boundFunc;

    // 确定执行上下文
    const thisContext = isNewCall ? this : Object(context || globalThis);

    // 合并参数
    const allArgs = bindArgs.concat(callArgs);

    // 调用原函数
    return originalFunc.apply(thisContext, allArgs);
  };

  // 维护原型链
  if (originalFunc.prototype) {
    // 使用 Object.create 来创建原型链, 避免直接修改
    boundFunc.prototype = Object.create(originalFunc.prototype);
    // 修正 constructor 指向
    boundFunc.prototype.constructor = boundFunc;
  }

  // 保留原函数的长度(可选,非标准)
  try {
    // 计算绑定函数的length属性(原函数参数个数 - 绑定的参数个数)
    const originalLength = originalFunc.length;
    const bindArgsLength = bindArgs.length;
    const remainingArgs = Math.max(originalLength - bindArgsLength, 0);

    // 使用 Object.defineProperty 来定义不可枚举的 length 属性
    Object.defineProperty(boundFunc, "length", {
      value: remainingArgs,
      writable: false,
      enumerable: false,
      configurable: true,
    });
  } catch (e) {
    // 忽略错误
  }

  // 保留原函数的name属性(可选)
  try {
    Object.defineProperty(boundFunc, "name", {
      value: `bound ${originalFunc.name || ""}`,
      writable: false,
      enumerable: false,
      configurable: true,
    });
  } catch (e) {
    // 忽略错误
  }

  return boundFunc;
};

ES6类实现版本

javascript 复制代码
class FunctionUtils {
  static callPolyfill(fn, context, ...args) {
    const fnKey = Symbol("fn");
    const target = context || globalThis;

    // 确保 context 是对象
    const contextObj =
      target === null || target === undefined ? globalThis : Object(target);

    context[fnKey] = fn;
    try {
      return contextObj[fnKey](...args);
    } finally {
      delete contextObj[fnKey];
    }
  }

  static applyPolyfill(fn, context, argsArray) {
    return this.callPolyfill(fn, context, ...(argsArray || []));
  }

  static bindPolyfill(fn, context, ...bindArgs) {
    return function (...callArgs) {
      const isNewCall = this instanceof fn;
      const thisContext = isNewCall ? this : context || globalThis;
      const allArgs = bindArgs.concat(callArgs);

      return fn.apply(thisContext, allArgs);
    };
  }
}

// 使用示例
function test(a, b) {
  console.log(this.value + a + b);
}

const obj = { value: 10 };
FunctionUtils.callPolyfill(test, obj, 1, 2); // 13
FunctionUtils.applyPolyfill(test, obj, [1, 2]); // 13

const boundTest = FunctionUtils.bindPolyfill(test, obj, 1);
boundTest(2); // 13

总结

  1. call/apply的核心:将函数临时添加到目标对象上,然后调用它
  2. bind的核心:返回一个新函数,闭包保存原函数、上下文和预置参数
  3. new操作符的处理:bind返回的函数被new调用时,this应该指向新创建的实例
  4. 原型链维护:bind返回的函数应该能正确继承原函数的原型
  5. 边界情况:处理null/undefined上下文、参数类型等
  6. 性能考虑:使用Symbol避免属性名冲突,使用try-finally确保清理
相关推荐
里欧跑得慢1 小时前
17. Flutter Hero动画实现:让界面过渡更加优雅
前端·css·flutter·web
IT_陈寒2 小时前
Vue的这个响应式陷阱,我debug了一整天才爬出来
前端·人工智能·后端
cn_mengbei2 小时前
用React Native开发OpenHarmony应用:Reanimated共享元素过渡
javascript·react native·react.js
kyriewen2 小时前
前端测试:别为了100%覆盖率而写测试,那是自欺欺人
前端·javascript·单元测试
去伪存真2 小时前
我自己写的第一个skills--project-core-standards
前端·agent
Data_Journal3 小时前
如何使用cURL更改User Agent
大数据·服务器·前端·javascript·数据库
掌心向暖RPA自动化3 小时前
如何获取网页某个元素在屏幕可见部分的中心坐标影刀RPA懒加载坐标定位技巧
java·javascript·自动化·rpa·影刀rpa
竹林8183 小时前
wagmi v2 多链钱包切换:一个 Uniswap 仿盘项目让我踩了三天坑
前端·javascript
donecoding3 小时前
Playwright MCP 页面捕获:Snapshot、截图、HTML 到底选哪个?
前端·ai编程·前端工程化
你也向往长安城吗3 小时前
最快的 JavaScript navmesh pathfinding3d 算法。
javascript