手写 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确保清理
相关推荐
WeiXiao_Hyy9 小时前
成为 Top 1% 的工程师
java·开发语言·javascript·经验分享·后端
吃杠碰小鸡10 小时前
高中数学-数列-导数证明
前端·数学·算法
kingwebo'sZone10 小时前
C#使用Aspose.Words把 word转成图片
前端·c#·word
xjt_090110 小时前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农10 小时前
Vue 2.3
前端·javascript·vue.js
夜郎king11 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳11 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵12 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星12 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_12 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js