手写实现 call、apply 与 bind:底层是如何改变函数执行上下文的?

各位开发者,下午好!

今天,我们将深入探讨 JavaScript 中三个看似简单却功能强大的方法:callapplybind。它们是 JavaScript 函数式编程和面向对象编程中不可或缺的工具,尤其是在处理函数执行上下文,也就是 this 关键字时。理解它们的底层工作机制,不仅能帮助我们更灵活地编写代码,还能加深对 JavaScript 运行时原理的理解。

我将带领大家手写实现这三个方法,并在此过程中详细剖析它们是如何改变函数执行上下文的。这不仅是一次编码练习,更是一次深入 JavaScript 语言核心的旅程。


一、 this 关键字:JavaScript 中动态的舞台主角

在深入 callapplybind 之前,我们必须先对 this 关键字有一个清晰的认识。this 是 JavaScript 中一个特殊且经常令人困惑的关键字,它的值在函数被调用时才确定,并且取决于函数的调用方式。这与许多其他语言中 this(或 self)的静态绑定行为截然不同。

JavaScript 中 this 的绑定规则主要有以下几种:

  1. 默认绑定 (Default Binding)
    当函数作为独立函数被调用,且不符合其他绑定规则时,this 会被绑定到全局对象。在浏览器环境中是 window,在 Node.js 环境中是 global。在严格模式下 ('use strict'),this 会被绑定到 undefined

    function showThis() {
        console.log(this);
    }
    
    showThis(); // 在浏览器中通常是 window,在 Node.js 中是 global(非严格模式)
                // 在严格模式下是 undefined
  2. 隐式绑定 (Implicit Binding)
    当函数作为某个对象的方法被调用时,this 会被绑定到那个对象。这是最常见的 this 绑定方式。

    const person = {
        name: 'Alice',
        greet: function() {
            console.log(`Hello, my name is ${this.name}`);
        }
    };
    
    person.greet(); // Hello, my name is Alice (this 绑定到 person 对象)
  3. 显式绑定 (Explicit Binding)
    我们可以使用 callapplybind 方法来强制将函数的 this 绑定到指定的对象。这正是我们今天的主题。

    function sayName() {
        console.log(`My name is ${this.name}`);
    }
    
    const anotherPerson = {
        name: 'Bob'
    };
    
    sayName.call(anotherPerson); // My name is Bob (this 被显式绑定到 anotherPerson)
  4. new 绑定 (new Binding)
    当函数作为构造函数与 new 关键字一起使用时,this 会被绑定到新创建的对象实例。

    function Dog(name) {
        this.name = name;
        console.log(`A new dog named ${this.name} is born!`);
    }
    
    const myDog = new Dog('Buddy'); // A new dog named Buddy is born! (this 绑定到 myDog 实例)
  5. 箭头函数绑定 (Lexical Binding for Arrow Functions)
    箭头函数没有自己的 this 绑定,它会捕获其外层(词法作用域)的 this 值。一旦确定,this 的值就不会再改变。

    const obj = {
        name: 'Charlie',
        sayHello: function() {
            const innerArrowFunc = () => {
                console.log(`Hello from ${this.name}`);
            };
            innerArrowFunc();
        }
    };
    
    obj.sayHello(); // Hello from Charlie (箭头函数捕获了 sayHello 方法的 this,即 obj)

理解这些规则是至关重要的,因为 callapplybind 正是为了提供对 this 绑定行为的精细控制。


二、 实现 Function.prototype.myCall:直接指定上下文并执行

call 方法允许我们立即调用一个函数,并指定该函数内部 this 的值。它的基本语法是 func.call(thisArg, arg1, arg2, ...)

核心思想:
要让一个函数 funcobj 的上下文中执行,我们可以暂时将 func 作为一个方法添加到 obj 上,然后通过 obj.func() 的方式调用它。这样,根据 JavaScript 的隐式绑定规则,func 内部的 this 就会指向 obj。调用完成后,我们再将这个临时添加的属性删除,以保持 obj 的原始状态。

实现步骤:

  1. 获取调用者函数: myCall 方法是挂载在 Function.prototype 上的,所以当它被调用时,this 关键字会指向调用 myCall 的那个函数本身。
  2. 处理 thisArg call 的第一个参数是 thisArg,即我们希望函数内部 this 指向的对象。
    • 如果 thisArgnullundefined,在非严格模式下,this 会被绑定到全局对象 (windowglobal)。在严格模式下,this 保持为 nullundefined。为了模拟标准行为,我们通常将其默认值设置为全局对象 globalThis(一个跨平台的全局对象引用)。
    • 如果 thisArg 是原始值(如字符串、数字、布尔值),它会被自动装箱(boxed)成对应的对象类型(例如,'hello' 会变成 new String('hello'))。
  3. 创建唯一键: 为了避免覆盖 thisArg 对象上已有的属性,我们需要生成一个独一无二的属性名来临时存储函数。Symbol 是一个非常适合此任务的 ES6 特性,因为它保证了唯一性。
  4. 挂载函数: 将获取到的函数作为 thisArg 的一个临时属性。
  5. 执行函数: 通过 thisArg[uniqueKey](arg1, arg2, ...) 的形式调用函数,此时 this 已经正确绑定。
  6. 保存结果: 存储函数执行的返回值。
  7. 清理:thisArg 对象中删除临时属性,以恢复其原始状态。
  8. 返回结果: 返回函数执行的结果。

代码实现:

为了跨环境兼容 globalThis,我们可以在代码开始前添加一个简单的 polyfill(如果环境不支持)。

// 简单的 globalThis polyfill,以确保在不同环境中都能访问到全局对象
// 在浏览器中是 window,在 Node.js 中是 global
if (typeof globalThis === 'undefined') {
    if (typeof window !== 'undefined') {
        globalThis = window;
    } else if (typeof global !== 'undefined') {
        globalThis = global;
    } else {
        // Fallback for very unusual environments, though less common
        globalThis = {};
    }
}

/**
 * 手写实现 Function.prototype.myCall
 * 允许一个函数在给定的 this 上下文和单独提供的参数下执行。
 *
 * @param {object} thisArg - 函数执行时 this 的值。
 * @param {...any} args - 传递给函数的参数列表。
 * @returns {any} 函数执行的返回值。
 */
Function.prototype.myCall = function(thisArg, ...args) {
    // 1. 'this' 指向调用 myCall 的函数本身。
    //    确保调用者是一个函数,否则抛出 TypeError。
    if (typeof this !== 'function') {
        throw new TypeError('Function.prototype.myCall - what is trying to be bound is not callable');
    }

    // 2. 处理 thisArg:
    //    如果 thisArg 为 null 或 undefined,将其设置为全局对象 (globalThis)。
    //    对于原始值类型 (string, number, boolean),将其包装成对应的对象 (Object(thisArg))。
    //    这样可以确保 this 始终是一个对象,或者在严格模式下保持 undefined。
    //    这里我们倾向于模拟非严格模式的全局对象行为。
    let context = thisArg;
    if (context === null || context === undefined) {
        context = globalThis; // 默认绑定到全局对象
    } else {
        context = Object(context); // 原始值装箱
    }

    // 3. 创建一个唯一的属性名,以避免与 context 对象上已有的属性发生命名冲突。
    //    使用 Symbol 是最佳实践,因为它保证了唯一性。
    const uniqueKey = Symbol('myCallTempFunc');

    // 4. 将当前函数(即 'this',调用 myCall 的函数)作为 context 对象的一个临时方法。
    context[uniqueKey] = this;

    // 5. 调用这个临时方法,此时函数内部的 'this' 将指向 context。
    //    使用 ES6 的扩展运算符 (...) 来传递参数列表。
    const result = context[uniqueKey](...args);

    // 6. 调用完成后,删除这个临时属性,以保持 context 对象的原始状态。
    delete context[uniqueKey];

    // 7. 返回函数执行的结果。
    return result;
};

示例与解释:

function greet(greeting, punctuation) {
    console.log(`${greeting}, ${this.name}${punctuation}`);
    return 'Done!';
}

const person1 = { name: 'Alice' };
const person2 = { name: 'Bob' };

// 使用 myCall 调用
console.log('--- Using myCall ---');
const res1 = greet.myCall(person1, 'Hello', '!'); // Hello, Alice!
console.log(`Result 1: ${res1}`); // Result 1: Done!

const res2 = greet.myCall(person2, 'Hi', '.');   // Hi, Bob.
console.log(`Result 2: ${res2}`); // Result 2: Done!

// ----------------------------------------------------
// 演示 thisArg 为 null/undefined 和原始值的情况

function showMyThis() {
    console.log('My this:', this);
}

console.log('n--- myCall with different thisArgs ---');

// thisArg 为 null 或 undefined
showMyThis.myCall(null);      // My this: [object Window] (在浏览器中) 或 [object global] (在Node.js中)
showMyThis.myCall(undefined); // My this: [object Window] (在浏览器中) 或 [object global] (在Node.js中)

// thisArg 为原始值会被装箱
showMyThis.myCall(123);       // My this: Number {123}
showMyThis.myCall('string');  // My this: String {'string'}
showMyThis.myCall(true);      // My this: Boolean {true}

// ----------------------------------------------------
// 严格模式下的 this 行为(myCall 默认模拟非严格模式下的全局对象)
// 如果要严格模拟严格模式,则 thisArg 为 null/undefined 时不应该被替换为 globalThis。
// 但对于 polyfill,通常为了兼容性会选择全局对象。
function strictShowMyThis() {
    'use strict';
    console.log('Strict My this:', this);
}
strictShowMyThis.myCall(null); // Strict My this: [object Window] (我们的myCall会将其转为globalThis)
                               // 原生call在严格模式下会是 Strict My this: null
                               // 这是 myCall 实现中一个与原生行为的细微差别,通常是为了简化或兼容性。

myCall 的实现精妙之处在于它利用了 JavaScript 的“点”操作符隐式绑定规则。通过临时将函数作为目标对象的一个方法,我们骗过了解释器,使其在调用时将 this 指向该目标对象。这种“借用”方法并即时执行的模式,是理解 JavaScript 运行时上下文切换的关键。


三、 实现 Function.prototype.myApply:通过数组传递参数

apply 方法与 call 方法的功能非常相似,它们都允许我们指定函数执行时的 this 值并立即执行函数。然而,它们在传递参数的方式上有所不同:call 接受一个参数列表,而 apply 接受一个参数数组。

核心思想:
myCall 相同,myApply 也是通过临时将函数作为目标对象的方法来实现 this 绑定的。主要的区别仅在于如何将参数传递给这个临时方法。

实现步骤:
myApply 的实现步骤与 myCall 大体一致,只有第 5 步(函数调用)略有不同。

  1. 获取调用者函数: this 指向调用 myApply 的函数。
  2. 处理 thisArgmyCall,处理 null/undefined 和原始值。
  3. 创建唯一键:myCall,使用 Symbol
  4. 挂载函数: 将当前函数作为 thisArg 的一个临时属性。
  5. 执行函数(不同点): myApply 接收一个参数数组。在调用临时方法时,我们需要使用扩展运算符 (...) 将这个数组展开,作为单独的参数传递给函数。
    • 需要注意,如果 argsArraynullundefined,则不应传递任何参数。
  6. 保存结果: 存储函数执行的返回值。
  7. 清理: 删除临时属性。
  8. 返回结果: 返回函数执行的结果。

代码实现:

/**
 * 手写实现 Function.prototype.myApply
 * 允许一个函数在给定的 this 上下文和数组形式的参数下执行。
 *
 * @param {object} thisArg - 函数执行时 this 的值。
 * @param {Array<any>} argsArray - 传递给函数的参数数组(可以是 null 或 undefined)。
 * @returns {any} 函数执行的返回值。
 */
Function.prototype.myApply = function(thisArg, argsArray) {
    // 1. 'this' 指向调用 myApply 的函数本身。
    if (typeof this !== 'function') {
        throw new TypeError('Function.prototype.myApply - what is trying to be bound is not callable');
    }

    // 2. 处理 thisArg:同 myCall。
    let context = thisArg;
    if (context === null || context === undefined) {
        context = globalThis;
    } else {
        context = Object(context);
    }

    // 3. 创建唯一的属性名。
    const uniqueKey = Symbol('myApplyTempFunc');

    // 4. 将当前函数作为 context 对象的一个临时方法。
    context[uniqueKey] = this;

    // 5. 调用这个临时方法。这里是 myApply 与 myCall 的主要区别:
    //    参数通过一个数组传递。使用扩展运算符将数组展开。
    //    如果 argsArray 不是一个数组或者为 null/undefined,不传递参数。
    let result;
    if (Array.isArray(argsArray)) {
        result = context[uniqueKey](...argsArray);
    } else if (argsArray === null || argsArray === undefined) {
        result = context[uniqueKey](); // 没有参数
    } else {
        // 如果 argsArray 存在但不是数组且不是 null/undefined,
        // 则与原生 apply 行为一致,抛出 TypeError。
        throw new TypeError('CreateListFromArrayLike called on non-object');
    }

    // 6. 调用完成后,删除临时属性。
    delete context[uniqueKey];

    // 7. 返回函数执行的结果。
    return result;
};

示例与解释:

function calculateSum(a, b, c) {
    console.log(`Context name: ${this.name}`);
    return a + b + c;
}

const calculator = { name: 'MyCalculator' };

console.log('n--- Using myApply ---');
const numbers = [10, 20, 30];
const total = calculateSum.myApply(calculator, numbers);
// Context name: MyCalculator
console.log(`Total sum: ${total}`); // Total sum: 60

const otherNumbers = [5, 5, 5];
const anotherTotal = calculateSum.myApply(calculator, otherNumbers);
// Context name: MyCalculator
console.log(`Another total sum: ${anotherTotal}`); // Another total sum: 15

// ----------------------------------------------------
// 演示 argsArray 为 null/undefined
function logArgs(...args) {
    console.log('Args:', args);
    console.log('This:', this);
}

console.log('n--- myApply with null/undefined argsArray ---');
logArgs.myApply(null, null);      // Args: [] , This: [object Window] (或 global)
logArgs.myApply({ id: 1 }, undefined); // Args: [] , This: { id: 1 }

// ----------------------------------------------------
// 演示 apply 的常见用例:数组操作
const arr = [1, 2, 3, 4, 5];
const maxVal = Math.max.myApply(null, arr); // 借用 Math.max 方法,将 this 设为 null (不重要),参数从数组中展开
console.log(`Max value in array: ${maxVal}`); // Max value in array: 5

// Array.prototype.slice.call(arguments) 是将类数组对象转换为数组的经典用法
function convertArgumentsToArray() {
    console.log(arguments); // [Arguments] { '0': 1, '1': 2, '2': 3 }
    const argsArray = Array.prototype.slice.myCall(arguments); // 借用 Array.prototype.slice
    console.log(argsArray); // [ 1, 2, 3 ]
    console.log(Array.isArray(argsArray)); // true
}
convertArgumentsToArray(1, 2, 3);

myApplymyCall 之间的选择通常取决于函数参数的来源。如果参数已经在一个数组中,apply 更方便;如果参数是逐个提供的,call 更直观。它们的底层机制在改变 this 上下文方面是完全相同的。


四、 实现 Function.prototype.myBind:返回一个新函数

bind 方法与 callapply 有着根本性的区别:它不会立即执行函数,而是返回一个全新的函数。这个新函数在将来被调用时,其 this 值会被永久绑定到 bind 方法的第一个参数,并且 bind 方法的后续参数会作为新函数的前置参数。

核心思想与挑战:

bind 的实现更为复杂,因为它需要处理两个主要场景:

  1. 普通函数调用: 当返回的绑定函数作为一个普通函数被调用时,其 this 应该指向 bind 时指定的 thisArg
  2. 构造函数调用: 当返回的绑定函数作为构造函数(即与 new 关键字一起使用)被调用时,其 this 不应该指向 bind 时指定的 thisArg,而是应该指向 new 关键字新创建的对象实例。同时,新创建的实例应该能够继承原函数的原型链。

实现步骤:

  1. 获取调用者函数: this 指向调用 myBind 的函数。
  2. 处理 thisArg 和初始参数: 存储 bind 方法传入的 thisArg 和所有前置参数。
  3. 返回一个新函数: 这是 bind 的核心。这个新函数将封装原函数的调用逻辑。
  4. 在新函数内部处理 this
    • 区分普通调用和 new 调用: 在新函数内部,需要判断它是被直接调用,还是通过 new 关键字作为构造函数调用。我们可以通过检查 this 是否是新函数的实例 (this instanceof FBound) 来判断。如果 thisFBound 的实例,说明是 new 调用。
    • new 调用: 如果是 new 调用,this 应该指向 new 关键字创建的新实例,而不是 bind 时传入的 thisArg
    • 普通调用: 如果是普通调用,this 应该指向 bind 时传入的 thisArg(如果为 null/undefined,则默认到 globalThis)。
  5. 合并参数: 新函数被调用时可能还会传入自己的参数。这些参数需要与 bind 时传入的初始参数合并,然后一起传递给原函数。
  6. 调用原函数: 使用 applycall 方法,以正确的 this 和合并后的参数调用原函数。
  7. 原型链继承 (Constructor Behavior): 当绑定函数作为构造函数使用时,new 出来的实例应该继承原函数的原型链。这意味着 FBound.prototype 应该与 originalFunc.prototype 建立正确的连接。一个常见且健壮的方式是让 FBound.prototype 继承自 originalFunc.prototype

代码实现:

/**
 * 手写实现 Function.prototype.myBind
 * 返回一个新函数,该新函数在被调用时,this 被绑定到指定的 thisArg,
 * 并且可以预置参数。新函数也可以作为构造函数使用。
 *
 * @param {object} thisArg - 函数执行时 this 的值。
 * @param {...any} initialArgs - 预置在原函数前的参数列表。
 * @returns {Function} 一个新的绑定函数。
 */
Function.prototype.myBind = function(thisArg, ...initialArgs) {
    // 1. 'this' 指向调用 myBind 的函数本身。
    const originalFunc = this;

    // 确保调用者是一个函数,否则抛出 TypeError。
    if (typeof originalFunc !== 'function') {
        throw new TypeError('Function.prototype.myBind - what is trying to be bound is not callable');
    }

    // 2. 返回一个新的函数 FBound。
    //    这个 FBound 将在被调用时,确保 originalFunc 以正确的上下文和参数执行。
    const FBound = function(...callArgs) {
        // console.log("FBound called. Current this:", this);
        // console.log("Is new call?", this instanceof FBound);

        // 3. 确定 originalFunc 内部的 'this' 值。
        //    核心逻辑:判断 FBound 是作为构造函数被调用 (使用 'new') 还是作为普通函数被调用。
        //    如果 'this' 是 FBound 的实例,说明它是通过 'new' 关键字调用的。
        //    在这种情况下,'this' 应该指向新创建的对象实例,而不是 myBind 传入的 thisArg。
        //    否则,'this' 就应该指向 myBind 传入的 thisArg。
        const isNewCall = this instanceof FBound;
        let context;

        if (isNewCall) {
            context = this; // 'this' 是 new 操作符创建的新实例
        } else {
            // 对于普通调用,使用 myBind 传入的 thisArg。
            // 同样需要处理 null/undefined 和原始值装箱。
            context = thisArg;
            if (context === null || context === undefined) {
                context = globalThis;
            } else {
                context = Object(context);
            }
        }

        // 4. 合并参数:
        //    myBind 传入的 initialArgs (预置参数) + FBound 调用时传入的 callArgs。
        const combinedArgs = initialArgs.concat(callArgs);

        // 5. 使用 apply 调用 originalFunc。
        //    apply 确保了正确的 'this' 绑定,并将 combinedArgs 作为数组传递。
        return originalFunc.apply(context, combinedArgs);
    };

    // 6. 处理原型链继承,以支持 FBound 作为构造函数时的行为。
    //    当 FBound 被 new 调用时,new FBound() 应该是一个 originalFunc 的实例,
    //    即 (new FBound()) instanceof originalFunc 应该为 true。
    //    这通过将 FBound 的原型链指向 originalFunc 的原型来实现。
    //    如果 originalFunc 有 prototype 属性(函数通常有),则让 FBound.prototype 继承自它。
    //    Object.create(originalFunc.prototype) 创建一个新对象,其原型是 originalFunc.prototype。
    //    这避免了直接修改 originalFunc.prototype,也避免了在设置原型时执行 originalFunc 构造函数。
    if (originalFunc.prototype) {
        FBound.prototype = Object.create(originalFunc.prototype);
    }
    // 注意:原生的 bind 还会处理 bound function 的 length 和 name 属性,
    // 但此处为了专注于 this 绑定核心逻辑,省略了这些辅助属性的实现。

    return FBound;
};

示例与解释:

1. 普通函数调用场景:

function greetPerson(greeting, punctuation) {
    console.log(`${greeting}, ${this.name}${punctuation}`);
    return `Greeting for ${this.name}`;
}

const user = { name: 'David' };
const boundedGreet = greetPerson.myBind(user, 'Hi'); // 绑定 this 为 user,预置参数 'Hi'

console.log('n--- myBind: Normal Function Call ---');
const result = boundedGreet('!!!'); // Hi, David!!!
console.log(`Bounded call result: ${result}`); // Bounded call result: Greeting for David

// 再次调用,this 仍然是 user
boundedGreet('.'); // Hi, David.

2. 构造函数调用场景:

这是 bind 最复杂的部分。当一个绑定函数被 new 调用时,它应该表现得像一个普通的构造函数,this 应该指向新创建的实例,而不是 bind 时指定的 thisArg

function Car(make, model) {
    this.make = make;
    this.model = model;
    console.log(`Car created: ${this.make} ${this.model}`);
}

Car.prototype.drive = function() {
    console.log(`${this.make} ${this.model} is driving.`);
};

const myCar = { make: 'Honda', model: 'Civic' };

// 尝试绑定 myCar 作为 thisArg,并预置 'Toyota' 作为 make
const ToyotaCarConstructor = Car.myBind(myCar, 'Toyota');

console.log('n--- myBind: Constructor Function Call ---');

// 使用 new 关键字调用绑定函数
const corolla = new ToyotaCarConstructor('Corolla');
// 预期输出:Car created: Toyota Corolla
// 实际输出:Car created: Toyota Corolla (this 绑定到新实例,而不是 myCar)

console.log(corolla.make);    // Toyota
console.log(corolla.model);   // Corolla
corolla.drive();              // Toyota Corolla is driving.

// 验证原型链
console.log(corolla instanceof Car);            // true (因为我们处理了原型链继承)
console.log(corolla instanceof ToyotaCarConstructor); // true
console.log(corolla instanceof Object);         // true

// 如果不使用 new 关键字,它会作为一个普通函数执行,this 绑定到 myCar
console.log('n--- myBind: Constructor Called as Normal Function ---');
const failCar = ToyotaCarConstructor('Prius'); // thisArg (myCar) 成为 this
// 预期输出:Car created: Toyota Prius
// 实际输出:Car created: Toyota Prius
// 注意:此时 this.make 和 this.model 会设置到 myCar 对象上,
// 而不是创建一个新的实例并返回。因为没有 new 关键字,它返回的是 Car 函数的返回值 (undefined)。
console.log(myCar); // { make: 'Toyota', model: 'Prius' }
console.log(failCar); // undefined (因为 Car 构造函数没有显式返回值)

myBindnew 关键字兼容性是其最巧妙的部分。通过判断 this instanceof FBound,我们能够在运行时动态决定 this 的最终绑定目标,并利用 Object.create 保证原型链的正确继承,使得绑定函数在作为构造函数时,仍然符合 JavaScript 的构造函数行为规范。


五、 call, apply, bind 对比总结

特性 call apply bind
this 绑定 显式绑定到第一个参数 thisArg 显式绑定到第一个参数 thisArg 返回的新函数将 this 永久绑定到 thisArg
参数传递 逐个列出 (comma-separated list) 以数组形式传递 (array) 预置参数,新函数被调用时可再追加参数
执行时机 立即执行 函数 立即执行 函数 不立即执行,返回一个新函数
返回值 被调用函数的返回值 被调用函数的返回值 一个新的函数
常用场景 快速改变 this 并执行,参数已知且不多 快速改变 this 并执行,参数已在数组中,
或不确定参数数量时
创建一个固定 this 和部分参数的函数,
常用于回调、事件处理、函数柯里化
构造函数行为 无(直接执行函数) 无(直接执行函数) 返回的函数可以用 new 调用,
此时 this 指向新实例

六、 底层机制:如何改变函数执行上下文的?

我们已经手写实现了 callapplybind,现在是时候总结一下它们改变函数执行上下文的底层机制了。

  1. callapply 的核心原理:隐式绑定规则的巧妙利用
    callapply 的实现原理非常相似,它们都利用了 JavaScript 中 this隐式绑定规则。当一个函数作为对象的方法被调用时,this 会自动指向那个对象。

    • 我们的 myCallmyApply 首先接收一个目标对象 thisArg
    • 然后,它们将待执行的函数(即 Function.prototype 上的 this临时地作为 thisArg 的一个属性。例如:thisArg[uniqueKey] = originalFunc;
    • 接着,它们通过 thisArg[uniqueKey]() 的方式来调用这个函数。此时,根据隐式绑定规则,originalFunc 内部的 this 自然就指向了 thisArg
    • 函数执行完毕后,这个临时属性会被立即删除,以保持 thisArg 的原始状态。
      这种机制简洁而高效,它没有“魔法”,而是巧妙地运用了语言内置的 this 决定规则。
  2. bind 的核心原理:闭包、new 行为模拟与原型链连接
    bind 则更为复杂,因为它返回的是一个新函数,而不是立即执行。它的底层机制涉及:

    • 闭包 (Closure): myBind 返回的 FBound 函数形成了一个闭包,它捕获了 myBind 调用时的 originalFuncthisArginitialArgs。这意味着即使 myBind 调用已经结束,FBound 仍然能够访问这些被捕获的变量。
    • 动态 this 决定: FBound 内部包含了一段逻辑,用于在它被调用时动态判断 this 的最终归属。它通过 this instanceof FBound 来检测是否是 new 关键字调用。
      • 如果是 new 调用,它会尊重 new 操作符的 this 绑定规则,让 this 指向新创建的实例。
      • 如果是普通调用,它会将 this 指向 myBind 传入的 thisArg
    • 参数柯里化 (Partial Application/Currying): FBoundmyBind 传入的 initialArgs 与它自己被调用时传入的 callArgs 合并,实现了参数的预置和灵活追加。
    • 原型链继承 (Prototype Chaining): 为了确保 new FBound() 实例能正确继承 originalFunc 的原型,myBind 会通过 Object.create(originalFunc.prototype)FBound.prototype 连接到 originalFunc.prototype。这使得 new FBound() instanceof originalFunc 能够返回 true,从而保持了构造函数行为的完整性。
      bind 本质上是一个“函数工厂”,它生产一个具备预设 this 和参数,并且能够兼容 new 调用的新函数。

七、 总结与展望

通过手写实现 callapplybind,我们不仅掌握了它们的使用方法,更重要的是,深入理解了它们如何利用 JavaScript 现有的 this 绑定规则和高级特性(如闭包、原型链)来精确控制函数执行上下文。这种对底层机制的理解,是成为一名真正 JavaScript 专家的基石。

这些方法是 JavaScript 灵活性的体现,它们赋予了我们强大的能力来重用函数、解耦代码、处理事件和构建复杂的应用程序。无论是处理事件回调中的 this 指向问题,还是实现函数柯里化,亦或是将类数组对象转换为真正的数组,callapplybind 都是我们工具箱中不可或缺的利器。希望今天的讲解能帮助大家对它们有更深刻的认识。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注