JavaScript 手写 call、apply、bind:深入理解函数上下文绑定

当面试官让我们手写 call、apply、bind 时,他们真正考察的是什么?这三个方法看似简单,却隐藏着 JavaScript 函数执行上下文、原型链、参数处理等核心概念。本文将从零实现,并深入理解它们的差异和应用场景。

前言:为什么需要 call、apply、bind?

javascript 复制代码
const obj = {
  name: '张三',
  sayHello() {
    console.log(`你好,我是${this.name}`);
  }
};

const sayHelloFunc = obj.sayHello;
obj.sayHello();     // "你好,我是张三" - 正确
sayHelloFunc();     // "你好,我是undefined" - this丢失了!

上述代码,出现问题根源是:函数的 this 在调用时才确定,取决于调用方式。那如何解决呢?使用call、apply、bind 显式绑定 this 。

call 方法的实现

call 的基本使用

call 方法用于调用一个函数,并显式指定函数的 this 值和参数列表。

javascript 复制代码
function greet(message) {
  console.log(`${message}, ${this.name}!`);
}

const person = { name: 'zhangsan' };

// 原生 call 的使用
greet.call(person, '你好'); // "你好, zhangsan!"

call 的工作原理

  1. 将函数设为对象的属性
  2. 使用该对象调用函数
  3. 删除该属性

基础版本实现

javascript 复制代码
Function.prototype.myCall = function (context, ...args) {
  // 如果context是null或undefined,则绑定到全局对象
  if (context == null) {
    context = globalThis;
  }
  // 给context对象添加一个临时属性,值为当前函数
  const fnKey = Symbol('fn'); // 使用Symbol避免属性名冲突
  context[fnKey] = this; // this指向调用myCall的函数
  // 使用context对象调用函数
  const result = context[fnKey](...args);
  // 删除临时属性
  delete context[fnKey];
  return result;
};

处理边界情况

javascript 复制代码
Function.prototype.myCallEnhanced = function (context, ...args) {
  // 处理undefined和null
  if (context == null) {
    context = globalThis;
  }

  // 原始值需要转换为对象,否则不能添加属性
  const contextType = typeof context;
  if (contextType === 'string' ||
    contextType === 'number' ||
    contextType === 'boolean' ||
    contextType === 'symbol' ||
    contextType === 'bigint') {
    context = Object(context); // 转换为包装对象
  }

  // 使用更安全的Symbol作为key
  const fnKey = Symbol('fn');
  context[fnKey] = this;

  try {
    const result = context[fnKey](...args);
    return result;
  } finally {
    // 确保总是删除临时属性
    delete context[fnKey];
  }
};

完整实现与性能优化

javascript 复制代码
Function.prototype.myCallFinal = function (context = globalThis, ...args) {
  // 1. 类型检查:确保调用者是函数
  if (typeof this !== 'function') {
    throw new TypeError('Function.prototype.myCallFinal called on non-function');
  }

  // 2. 处理Symbol和BigInt(ES6+)
  const contextType = typeof context;
  let finalContext = context;

  // 3. 处理原始值(非严格模式下的自动装箱)
  if (contextType === 'string') {
    finalContext = new String(context);
  } else if (contextType === 'number') {
    finalContext = new Number(context);
  } else if (contextType === 'boolean') {
    finalContext = new Boolean(context);
  } else if (contextType === 'symbol') {
    // Symbol不能通过new创建,使用Object
    finalContext = Object(context);
  } else if (contextType === 'bigint') {
    // BigInt不能通过new创建,使用Object
    finalContext = Object(context);
  }
  // null和undefined已经通过默认参数处理

  // 4. 使用Symbol创建唯一key,避免属性冲突
  const fnSymbol = Symbol('callFn');

  // 5. 将函数绑定到上下文对象
  // 使用Object.defineProperty确保属性可配置
  Object.defineProperty(finalContext, fnSymbol, {
    value: this,
    configurable: true,
    writable: true,
    enumerable: false
  });

  // 6. 执行函数并获取结果
  let result;
  try {
    result = finalContext[fnSymbol](...args);
  } finally {
    // 7. 清理临时属性
    try {
      delete finalContext[fnSymbol];
    } catch (error) {
      // 如果上下文不可配置,忽略错误
      console.warn('无法删除临时属性:', error.message);
    }
  }

  return result;
};

apply 方法的实现

apply 的基本使用

apply 和 call 的功能基本相同,唯一的区别在于参数的传递方式:

  • call 接受参数列表
  • apply 接受参数数组
javascript 复制代码
function sum(a, b, c) {
  return a + b + c;
}
// apply:参数以数组形式传递
sum.apply(null, [1, 2, 3]);

基础版本实现

javascript 复制代码
Function.prototype.myCall = function (context, args) {
  // 如果context是null或undefined,则绑定到全局对象
  if (context == null) {
    context = globalThis;
  }
  // 给context对象添加一个临时属性,值为当前函数
  const fnKey = Symbol('fn'); // 使用Symbol避免属性名冲突
  context[fnKey] = this; // this指向调用myCall的函数
  // 使用context对象调用函数
  const result = context[fnKey](...args);
  // 删除临时属性
  delete context[fnKey];
  return result;
};

完整实现与性能优化

javascript 复制代码
Function.prototype.myApply = function (context = globalThis, argsArray) {
  // 1. 类型检查
  if (typeof this !== 'function') {
    throw new TypeError('Function.prototype.myApply called on non-function');
  }

  // 2. 参数处理:确保argsArray是数组或类数组对象
  let args = [];
  if (argsArray != null) {
    // 检查是否为数组或类数组
    if (typeof argsArray !== 'object' ||
      (typeof argsArray.length !== 'number' && argsArray.length !== undefined)) {
      throw new TypeError('第二个参数必须是数组或类数组对象');
    }

    // 将类数组转换为真实数组
    if (!Array.isArray(argsArray)) {
      args = Array.from(argsArray);
    } else {
      args = argsArray;
    }
  }

  // 3. 使用Symbol作为唯一key
  const fnSymbol = Symbol('applyFn');

  // 4. 处理原始值(与call相同)
  const contextType = typeof context;
  let finalContext = context;

  if (contextType === 'string') {
    finalContext = new String(context);
  } else if (contextType === 'number') {
    finalContext = new Number(context);
  } else if (contextType === 'boolean') {
    finalContext = new Boolean(context);
  } else if (contextType === 'symbol') {
    finalContext = Object(context);
  } else if (contextType === 'bigint') {
    finalContext = Object(context);
  }

  // 5. 绑定函数到上下文
  Object.defineProperty(finalContext, fnSymbol, {
    value: this,
    configurable: true,
    writable: true,
    enumerable: false
  });

  // 6. 执行函数
  let result;
  try {
    result = finalContext[fnSymbol](...args);
  } finally {
    // 7. 清理
    try {
      delete finalContext[fnSymbol];
    } catch (error) {
      // 忽略删除错误
    }
  }

  return result;
};

bind 方法的实现

bind 的基本使用

bind 方法创建一个新的函数,当这个新函数被调用时,它的 this 值会被绑定到指定的对象,并且可以预先传入部分参数。

javascript 复制代码
function greet(greeting, name) {
  console.log(`${greeting}, ${name}! 我是${this.role}`);
}

const context = { role: '管理员' };

// bind:创建新函数,稍后执行
const boundGreet = greet.bind(context, '你好');
boundGreet('李四'); 

bind 的核心特性:

  1. 返回一个新函数
  2. 可以预设参数(柯里化)
  3. 绑定this值
  4. 支持new操作符(特殊情况)

基础版本实现

javascript 复制代码
Function.prototype.myBind = function (context, ...bindArgs) {
  const fn = this;
    return function(...newArgs) {
        return fn.apply(context, [...args, ...newArgs]);
    };
};

处理 new 操作符的特殊情况

javascript 复制代码
Function.prototype.myBindEnhanced = function (context = globalThis, ...bindArgs) {
  const originalFunc = this;

  if (typeof originalFunc !== 'function') {
    throw new TypeError('Function.prototype.myBindEnhanced called on non-function');
  }

  // 内部函数,用于判断是否被new调用
  const boundFunc = function (...callArgs) {
    // 关键判断:this instanceof boundFunc
    // 如果使用new调用,this会是boundFunc的实例
    const isConstructorCall = this instanceof boundFunc;

    // 确定最终的上下文
    // 如果是构造函数调用,使用新创建的对象作为this
    // 否则使用绑定的context
    const finalContext = isConstructorCall ? this : Object(context);

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

    // 执行原函数
    // 如果原函数有返回值,需要特殊处理
    const result = originalFunc.apply(finalContext, finalArgs);

    // 构造函数调用的特殊处理
    // 如果原函数返回一个对象,则使用该对象
    // 否则返回新创建的对象(this)
    if (isConstructorCall) {
      if (result && (typeof result === 'object' || typeof result === 'function')) {
        return result;
      }
      return this;
    }

    return result;
  };

  // 维护原型链
  // 方法1:直接设置prototype(有缺陷)
  // boundFunc.prototype = originalFunc.prototype;

  // 方法2:使用空函数中转(推荐)
  const F = function () { };
  F.prototype = originalFunc.prototype;
  boundFunc.prototype = new F();
  boundFunc.prototype.constructor = boundFunc;

  // 添加一些元信息(可选)
  boundFunc.originalFunc = originalFunc;
  boundFunc.bindContext = context;
  boundFunc.bindArgs = bindArgs;

  return boundFunc;
};

完整实现与性能优化

javascript 复制代码
Function.prototype.myBindFinal = (function () {
  // 使用闭包保存Slice方法,提高性能
  const ArraySlice = Array.prototype.slice;

  // 空函数,用于原型链维护
  function EmptyFunction() { }

  return function myBindFinal(context = globalThis, ...bindArgs) {
    const originalFunc = this;

    // 严格的类型检查
    if (typeof originalFunc !== 'function') {
      throw new TypeError('Function.prototype.bind called on non-function');
    }

    // 处理原始值的上下文(非严格模式)
    let boundContext = context;
    const contextType = typeof boundContext;

    // 原始值包装(与call/apply保持一致)
    if (contextType === 'string') {
      boundContext = new String(boundContext);
    } else if (contextType === 'number') {
      boundContext = new Number(boundContext);
    } else if (contextType === 'boolean') {
      boundContext = new Boolean(boundContext);
    } else if (contextType === 'symbol') {
      boundContext = Object(boundContext);
    } else if (contextType === 'bigint') {
      boundContext = Object(boundContext);
    }

    // 创建绑定函数
    const boundFunction = function (...callArgs) {
      // 判断是否被new调用
      const isConstructorCall = this instanceof boundFunction;

      // 确定最终上下文
      let finalContext;
      if (isConstructorCall) {
        // new调用:忽略绑定的context,使用新实例
        finalContext = this;
      } else if (boundContext == null) {
        // 非严格模式:使用全局对象
        finalContext = globalThis;
      } else {
        // 普通调用:使用绑定的context
        finalContext = boundContext;
      }

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

      // 调用原函数
      const result = originalFunc.apply(finalContext, allArgs);

      // 处理构造函数调用的返回值
      if (isConstructorCall) {
        // 如果原函数返回对象,则使用该对象
        if (result && (typeof result === 'object' || typeof result === 'function')) {
          return result;
        }
        // 否则返回新创建的实例
        return this;
      }

      return result;
    };

    // 维护原型链 - 高性能版本
    // 避免直接修改boundFunction.prototype,使用中间函数
    if (originalFunc.prototype) {
      EmptyFunction.prototype = originalFunc.prototype;
      boundFunction.prototype = new EmptyFunction();
      // 恢复constructor属性
      boundFunction.prototype.constructor = boundFunction;
    } else {
      // 处理没有prototype的情况(如箭头函数)
      boundFunction.prototype = undefined;
    }

    // 添加不可枚举的原始函数引用(用于调试)
    Object.defineProperty(boundFunction, '__originalFunction__', {
      value: originalFunc,
      enumerable: false,
      configurable: true,
      writable: true
    });

    // 添加不可枚举的绑定信息
    Object.defineProperty(boundFunction, '__bindContext__', {
      value: boundContext,
      enumerable: false,
      configurable: true,
      writable: true
    });

    Object.defineProperty(boundFunction, '__bindArgs__', {
      value: bindArgs,
      enumerable: false,
      configurable: true,
      writable: true
    });

    // 设置适当的函数属性
    Object.defineProperty(boundFunction, 'length', {
      value: Math.max(0, originalFunc.length - bindArgs.length),
      enumerable: false,
      configurable: true,
      writable: false
    });

    Object.defineProperty(boundFunction, 'name', {
      value: `bound ${originalFunc.name || ''}`.trim(),
      enumerable: false,
      configurable: true,
      writable: false
    });

    return boundFunction;
  };
})();

面试常见问题与解答

问题1:手写call的核心步骤是什么?

  1. 步骤1: 将函数设为上下文对象的属性
  2. 步骤2: 执行该函数
  3. 步骤3: 删除该属性
  4. 步骤4: 返回函数执行结果
  5. 关键点:
    • 使用Symbol避免属性名冲突
    • 处理null/undefined上下文
    • 处理原始值上下文
    • 使用展开运算符处理参数

问题2:bind如何处理new操作符?

  1. 通过 this instanceof boundFunction 判断是否被new调用
  2. 如果是new调用,忽略绑定的上下文,使用新创建的对象作为this
  3. 需要正确设置boundFunction的原型链,以支持instanceof
  4. 如果原构造函数返回对象,则使用该对象,否则返回新实例

问题3:call、apply、bind的性能差异?

  1. call通常比apply快,因为apply需要处理数组参数
  2. bind创建新函数有开销,但多次调用时比重复call/apply高效

结语

通过深入理解call、apply、bind的实现原理,我们不仅能更好地回答面试问题,还能在实际开发中编写出更优雅、更高效的JavaScript代码。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

相关推荐
wordbaby2 小时前
Vue 实战:从零实现“划词标注”与“高亮笔记”功能
前端
上海合宙LuatOS2 小时前
LuatOS核心库API——【fatfs】支持FAT32文件系统
java·前端·网络·数据库·单片机·嵌入式硬件·物联网
wuhen_n2 小时前
JavaScript 手写 new 操作符:深入理解对象创建
前端·javascript
m0_528749002 小时前
linux编程----目录流
java·前端·数据库
集成显卡2 小时前
前端视频播放方案选型:主流 Web 播放器对比 + Vue3 实战
前端·vue·音视频
前端 贾公子2 小时前
Vue3 业务组件库按需加载的实现原理(中)
前端·javascript·vue.js
温轻舟2 小时前
前端可视化大屏【附源码】
前端·javascript·css·html·可视化·可视化大屏·温轻舟
北极象2 小时前
Flying-Saucer HTML到PDF渲染引擎核心流程分析
前端·pdf·html
weixin199701080162 小时前
Tume商品详情页前端性能优化实战
大数据·前端·java-rabbitmq
梦里寻码2 小时前
深入解析 SmartChat 的 RAG 架构设计 — 如何用 pgvector + 本地嵌入打造企业级智能客服
前端·agent