手写call、apply、bind方法的进阶之旅

121 阅读5分钟

在上一篇文章中,我们初步探讨了 call 方法的手写实现,以及严格模式与非严格模式下的差异。今天,我们将基于之前的内容,进一步完善 call、apply 和 bind 这三个方法的实现,让它们能在两种模式下都稳定运行。

一、完善 call 方法

之前我们实现的 call 方法已经考虑了一些基本情况,但还有可以优化的地方。让我们来看一个更完善的版本:

Function.prototype.myCall = function(context, ...args) {
    if (typeof this !== 'function') {
        throw new TypeError('Function.prototype.myCall called on non-function');
    }

    // 处理Node环境和浏览器环境
    const globalObj = typeof window !== 'undefined' ? window : global;
    context = context === null || context === undefined ? globalObj : Object(context);

    const fnKey = Symbol('fn');
    Object.defineProperty(context, fnKey, {
        value: this,
        configurable: true,
        enumerable: false
    });

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

这个实现有几个值得注意的点:

  1. 跨环境兼容性:通过检测window对象是否存在来判断当前环境是浏览器还是 Node.js,分别使用windowglobal作为全局对象。
  2. 临时属性的优化
    • 使用Symbol创建唯一的临时属性名,避免与已有属性冲突
    • 设置enumerable: false确保该属性不会被枚举,减少对目标对象的影响
    • 使用try...finally确保无论函数执行结果如何,临时属性都会被删除
  3. context 处理
    • nullundefined统一转换为全局对象
    • 使用Object(context)将原始值(如数字、字符串)包装为对象,确保可以挂载临时方法

二、apply 方法的完善实现

apply 方法和 call 方法类似,主要区别在于参数的传递方式。apply 接受一个数组作为参数,而 call 接受一系列单独的参数。让我们来看 apply 的完善实现:

Function.prototype.myApply = function(context, argsArray) {
    if (typeof this !== 'function') {
        throw new TypeError('Function.prototype.myApply called on non-function');
    }

    // 类数组检查
    if (argsArray !== undefined && argsArray !== null && 
        (!(typeof argsArray === 'object') || !('length' in argsArray))) {
        throw new TypeError('CreateListFromArrayLike called on non-object');
    }

    const globalObj = typeof window !== 'undefined' ? window : global;
    context = context === null || context === undefined ? globalObj : Object(context);

    const fnKey = Symbol('fn');
    Object.defineProperty(context, fnKey, {
        value: this,
        configurable: true,
        enumerable: false
    });

    try {
        // 转换类数组为真实数组
        const args = argsArray ? Array.from(argsArray) : [];
        return context[fnKey](...args);
    } finally {
        delete context[fnKey];
    }
};

这个实现和 myCall 很相似,但有几个关键区别:

  1. 类数组参数校验
    • 检查argsArray是否为对象且包含length属性
    • 符合 ES 规范中对 apply 方法第二个参数的要求
  2. 类数组转换
    • 使用Array.from(argsArray)将类数组对象转换为真实数组
    • 确保可以安全地使用扩展运算符...展开参数
  3. 错误处理
    • 当传入非类数组参数时抛出符合规范的错误信息
    • 与原生 apply 方法的错误处理保持一致

在实际使用中,apply 方法特别适合当我们的参数已经是一个数组,或者需要将类数组对象(如 arguments)作为参数传递的场景。例如:

const numbers = [1, 2, 3, 4, 5];
const max = Math.max.myApply(null, numbers);
console.log(max); // 5

三、bind 方法的完善实现

bind 方法和 call、apply 有所不同,它不会立即执行函数,而是返回一个新的函数,这个新函数的 this 值被永久绑定到了 bind 的第一个参数上。让我们来看 bind 的完善实现:

Function.prototype.myBind = function(context, ...args) {
    if (typeof this !== 'function') {
        throw new TypeError('Function.prototype.myBind called on non-function');
    }

    const self = this;
    const globalObj = typeof window !== 'undefined' ? window : global;
    const boundContext = context === null || context === undefined ? globalObj : Object(context);

    const boundFunction = function(...newArgs) {
        // 使用instanceof检测构造函数调用
        if (this instanceof boundFunction) {
            return new self(...args, ...newArgs);
        }
        return self.apply(boundContext, [...args, ...newArgs]);
    };

    // 维护原型链
    if (this.prototype) {
        boundFunction.prototype = Object.create(this.prototype);
    }

    return boundFunction;
};

这个实现有几个关键特点:

  1. 构造函数检测
    • 使用this instanceof boundFunction判断函数是否通过new关键字调用
    • 当作为构造函数使用时,忽略绑定的 context,确保this指向新创建的实例
  2. 原型链维护
    • 通过Object.create(this.prototype)创建新的原型对象
    • 确保绑定函数作为构造函数使用时,实例能正确继承原始函数的原型属性
  3. 参数合并
    • 支持柯里化(currying),即多次传递参数
    • bind时的参数与调用时的参数会合并并传递给原始函数

bind 方法非常适合需要延迟执行函数,或者需要将函数作为回调传递但又想固定 this 值的场景。例如:

const person = {
  name: '张三',
  sayHello: function(greeting) {
    console.log(`${greeting}, 我是${this.name}`);
  }
};

const sayHello = person.sayHello.myBind({name: '李四'}, '你好');
sayHello(); // "你好, 我是李四"

四、严格模式与非严格模式的对比总结

为了更清晰地展示这三个方法在严格模式和非严格模式下的行为差异,我们来做一个总结:

方法严格模式行为非严格模式行为
callcontext 为 null/undefined 时,this 为 null/undefined;原始值不被包装context 为 null/undefined 时,this 指向 window;原始值会被包装成对象
apply同上;第二个参数必须是数组或 undefined,否则抛出错误同上;对第二个参数的类型要求较宽松
bind返回的函数中,this 始终是绑定的 context;作为构造函数调用时,this 指向实例返回的函数中,context 为 null/undefined 时,this 指向 window;作为构造函数调用时,this 指向实例

理解这些差异对于正确使用这些方法非常重要。严格模式提供了更可预测的行为,减少了隐式转换,有助于写出更高质量的代码。

通过实现这三个方法,我们不仅更深入地理解了它们的工作原理,也对 JavaScript 中的 this 绑定、函数调用、严格模式等概念有了更清晰的认识。

希望这篇文章能帮助你更好地掌握这些重要的 JavaScript 方法。如果你有任何疑问或发现任何错误,欢迎在评论区留言讨论!