手写实现 Function.prototype.bind 的复杂兼容性:处理 new 操作符与多层参数绑定

各位编程领域的同仁们,大家好!

今天,我们将深入探讨 JavaScript 中一个看似简单实则蕴含巨大复杂性的内置函数:Function.prototype.bind。我们不仅会剖析它的基本用法,更将层层递进,挑战其在 new 操作符下的行为,以及多层参数绑定的精妙机制。我们的目标是手写实现一个具备高度兼容性的 bind 函数,足以应对各种复杂的场景。

Function.prototype.bind:初探与核心价值

在 JavaScript 中,函数的 this 上下文是动态的,它取决于函数被调用的方式。这种灵活性在某些场景下会带来不便,例如在事件处理器、回调函数或面向对象编程中,我们可能需要固定函数的 this 值。bind 正是为了解决这一痛点而生。

bind 方法会创建一个新的函数,当这个新函数被调用时,它的 this 值会被绑定到 bind 的第一个参数上,同时,bind 的其余参数会作为新函数的前置参数。

基本语法:

func.bind(thisArg, arg1, arg2, ...)
  • thisArg: 当绑定函数被调用时,这个值会被作为 this 上下文。
  • arg1, arg2, ...: 当绑定函数被调用时,这些参数会作为原函数的前置参数。

让我们看一个简单的例子:

const person = {
    name: "Alice",
    greet: function (greeting) {
        console.log(`${greeting}, my name is ${this.name}`);
    }
};

const unboundGreet = person.greet;
unboundGreet("Hello"); // 输出: Hello, my name is undefined (因为此时this指向全局对象)

const boundGreet = person.greet.bind(person);
boundGreet("Hi"); // 输出: Hi, my name is Alice (this被绑定到person)

const anotherPerson = { name: "Bob" };
const boundToBobGreet = person.greet.bind(anotherPerson, "Good morning");
boundToBobGreet(); // 输出: Good morning, my name is Bob (this绑定到anotherPerson, 参数部分应用)

从这个例子中,我们可以提炼出 bind 的两个核心功能:

  1. 永久绑定 this 上下文。
  2. 实现函数的部分应用(Partial Application),或称柯里化(Currying)的一种形式。

第一次尝试:基础 this 绑定与参数预设

我们首先尝试实现一个最基础的 myBind 版本,它能够处理 this 绑定和部分参数预设。

Function.prototype.myBind = function (context, ...args1) {
    const func = this; // 'this' 指向原始函数

    // 返回一个新函数
    return function (...args2) {
        // 使用 apply 调用原始函数,并将 context 作为 this
        // 将 bind 时的参数 (args1) 和调用新函数时的参数 (args2) 合并
        return func.apply(context, args1.concat(args2));
    };
};

代码解析:

  1. const func = this;: 在 myBind 被调用时,this 指向调用 myBind 的那个函数(即原始函数)。我们将其保存到 func 变量中。
  2. ...args1: 这是 bind 方法接收到的除 context 之外的所有参数,它们将被预先绑定。
  3. return function (...args2) { ... };: bind 总是返回一个新的函数。当这个新函数被调用时,它可能接收到新的参数 args2
  4. func.apply(context, args1.concat(args2));: 这是核心。我们使用 apply 来调用原始函数 func
    • 第一个参数 context 确保了 this 的绑定。
    • 第二个参数 args1.concat(args2)bind 时传入的参数 args1 和新函数被调用时传入的参数 args2 合并成一个数组,作为 func 的最终参数。

测试基础功能:

const obj = { x: 42 };

function sum(a, b, c) {
    return this.x + a + b + c;
}

const boundSum = sum.myBind(obj, 10);
console.log(boundSum(5, 3)); // 期望: 42 + 10 + 5 + 3 = 60

const anotherObj = { x: 100 };
const boundSum2 = sum.myBind(anotherObj, 1, 2);
console.log(boundSum2(3)); // 期望: 100 + 1 + 2 + 3 = 106

这个基础版本已经能够处理 this 绑定和参数预设,但它忽略了 bind 的一个关键行为:当绑定函数作为构造函数(即使用 new 操作符)被调用时,this 的行为会发生变化。

深入理解 new 操作符与 bind 的冲突

new 操作符在 JavaScript 中用于创建一个新的对象实例。当一个函数被 new 调用时,会发生以下几个步骤:

  1. 创建一个新的空对象。
  2. 将这个新对象的 [[Prototype]] 链接到构造函数的 prototype 属性。
  3. 将构造函数的作用域赋给新的 this 上下文。 也就是说,在构造函数内部,this 会指向这个新创建的对象。
  4. 执行构造函数。 构造函数可能会修改 this 对象。
  5. 如果构造函数没有显式返回一个对象,则默认返回新创建的 this 对象。 如果构造函数显式返回了一个对象,那么这个返回的对象将代替 this 对象成为 new 表达式的结果。

现在问题来了:如果我们将一个函数 F 通过 bind 绑定了 this,得到了 BoundF。然后我们尝试 new BoundF()。此时,BoundF 内部的 this 应该指向 new 创建的新实例,而不是 bind 时指定的 context

示例:

function Person(name) {
    this.name = name;
}

const boundPerson = Person.bind(null, "Alice"); // 绑定了null作为this,并预设了参数

const p1 = new boundPerson();
console.log(p1.name); // 期望: Alice (此时bind的this (null) 应该被忽略,name应该通过构造函数设置)
console.log(p1 instanceof Person); // 期望: true

我们之前的 myBind 版本在 new boundPerson() 时会出问题。func.apply(context, ...) 会强制将 this 设置为 context (这里是 null),导致 this.name = name 实际上是 null.name = "Alice",这会抛出错误。即使不报错,p1 也不会是 Person 的实例。

bindnew 兼容性规则:

当绑定函数作为构造函数(使用 new)被调用时:

  1. bind 传入的 thisArg 将被完全忽略。
  2. this 上下文会指向 new 操作符创建的新实例。
  3. 绑定函数会像普通构造函数一样执行,并且其 prototype 链也会被正确维护。

第二次尝试:处理 new 操作符

为了实现 new 兼容性,我们需要解决两个核心问题:

  1. 在绑定函数被 new 调用时,忽略 bind 传入的 thisArg,并使用 new 提供的 this
  2. 确保绑定函数的实例能够正确地继承原始函数的 prototype

检测 new 调用

如何判断返回的 fBound 函数是被普通调用还是被 new 调用?

在返回的 fBound 函数内部,this 的值在两种情况下是不同的:

  • 普通调用: this 通常是 window (非严格模式) 或 undefined (严格模式),或者是由 call/apply 显式设置的值。
  • new 调用: this 是一个新创建的对象,并且这个对象的 [[Prototype]] 链接到了 fBound.prototype。因此,this instanceof fBound 将会是 true

利用 this instanceof fBound 这一特性,我们可以判断当前 fBound 是否作为构造函数被调用。

维护原型链

为了让 new BoundPerson() 得到的实例 p1 能够通过 p1 instanceof Person 检查,我们需要确保 BoundPerson.prototype 能够正确继承 Person.prototype

简单的 BoundPerson.prototype = Person.prototype 是不行的,因为这样会直接修改 Person.prototype。我们需要一种方式来间接继承。

常见的做法是使用一个中间函数(空操作函数,No-Op Function):

  1. 创建一个空函数 fNOP
  2. fNOP.prototype 设置为原始函数 func.prototype
  3. fBound.prototype 设置为 new fNOP()。这样 fBound.prototype 就拥有了 func.prototype 的所有属性,并且不会影响 func.prototype 本身。

更现代的做法是使用 Object.create()

Object.create(func.prototype) 可以直接创建一个新对象,并将其 [[Prototype]] 链接到 func.prototype,这比 new fNOP() 更简洁和安全,因为它避免了调用 fNOP 的构造函数(即使是空函数也可能被认为有构造函数)。

实现细节:

Function.prototype.myBind = function (context, ...args1) {
    const func = this; // 原始函数

    // 1. 如果 func 不是一个函数,抛出 TypeError
    if (typeof func !== 'function') {
        throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }

    const fBound = function (...args2) {
        // 2. 核心:判断是否通过 new 调用
        // 如果当前 this 是 fBound 的实例,说明是通过 new 调用的
        // 此时应该使用 this (新创建的实例) 作为上下文,否则使用 context
        const actualContext = this instanceof fBound ? this : context;

        // 合并 bind 时的参数和调用时的参数
        // 并用 apply 调用原始函数
        return func.apply(actualContext, args1.concat(args2));
    };

    // 3. 维护原型链:
    // 创建一个继承自 func.prototype 的新对象,并将其赋给 fBound.prototype
    // 这样,通过 new fBound() 创建的实例就能正确继承 func 的原型链
    // 并且不会直接修改 func.prototype
    if (func.prototype) { // 只有函数有 prototype 属性才需要继承
        fBound.prototype = Object.create(func.prototype);
    }

    return fBound;
};

测试 new 兼容性:

function Vehicle(type) {
    this.type = type || 'unknown';
}
Vehicle.prototype.getModel = function () {
    return `Model: ${this.type}`;
};

const Car = Vehicle.myBind(null, 'Sedan'); // 绑定了 type 参数
const myCar = new Car();
console.log(myCar.type); // 期望: Sedan
console.log(myCar.getModel()); // 期望: Model: Sedan
console.log(myCar instanceof Vehicle); // 期望: true
console.log(myCar instanceof Car);     // 期望: true

// 如果原始函数返回一个对象,new 操作符应该返回该对象
function ConstructorReturnsObject(value) {
    this.initialValue = value;
    return { overriddenValue: value * 2 };
}
const BoundConstructorReturnsObject = ConstructorReturnsObject.myBind(null, 10);
const instance = new BoundConstructorReturnsObject();
console.log(instance.initialValue);      // 期望: undefined (因为返回了新对象)
console.log(instance.overriddenValue);   // 期望: 20
console.log(instance instanceof ConstructorReturnsObject); // 期望: false

这个版本已经相当完善,能够处理 this 绑定、参数预设和 new 操作符。然而,bind 还有一个更复杂的行为:多层参数绑定。

终极挑战:多层参数绑定(Multi-level Binding)

想象一下这样的场景:

function log(level, message) {
    console.log(`[${level}] ${message}`);
}

const warn = log.bind(null, "WARN");
const error = warn.bind(null, "ERROR"); // 这有问题,warn已经绑定了WARN

我们期望 error("Something failed") 能够输出 [ERROR] Something failed,但根据我们目前的 myBind 实现,warn 已经把 level 参数绑定为 "WARN"。如果再对 warn 调用 bind,它会尝试再次绑定 level,导致行为不符合预期。

原生 bind 的规则是:

  1. this 绑定是单向的,不可覆盖。 如果一个函数已经被 bind 绑定了 this,那么对它再次调用 bind 时,新的 thisArg 参数会被忽略,它将保持第一次绑定时的 this
  2. 参数绑定是累加的。 每次调用 bind 都会在之前绑定的参数基础上,追加新的预设参数。

这意味着,当 log.bind(null, "WARN") 返回一个绑定函数 warn 后,warn 内部会记住它是由 log 绑定而来,且其 this 绑定是 null,第一个参数是 "WARN"

warn.bind(null, "ERROR") 被调用时,它会:

  • 忽略新的 thisArg (null),因为它已经从 log.bind 继承了 nullthis 绑定。
  • 累加参数:在 ["WARN"] 的基础上,追加 ["ERROR"],最终参数列表变为 ["WARN", "ERROR"]

显然,这与我们期望的 [ERROR] Something failed 不符。原生 bind 的行为是,如果一个函数已经是绑定函数,那么对其再次 bind 时,它会获取其内部 [[BoundTargetFunction]](原始目标函数)、[[BoundThis]][[BoundArguments]],然后用新的参数扩充 [[BoundArguments]],但 [[BoundThis]] 保持不变。

因此,log.bind(null, "WARN").bind(null, "ERROR") 最终会绑定 lognull,参数为 ["WARN", "ERROR"]

要实现 error("Something failed") 输出 [ERROR] Something failed,我们应该这样绑定:

const logger = function(level, msg) {
    console.log(`[${level}] ${msg}`);
};

const warn = logger.bind(null, "WARN");
const error = logger.bind(null, "ERROR"); // 应该直接绑定到 logger,而不是 warn

但是,如果用户确实写了 const error = warn.bind(null, "ERROR"); 呢?我们的 myBind 必须正确模拟原生 bind 的行为。

原生 bind 的内部机制(概念性):

JavaScript 引擎在创建绑定函数时,会在其内部存储一些特殊属性(称为“内部插槽”,如 [[BoundTargetFunction]], [[BoundThis]], [[BoundArguments]])。这些插槽是不可直接访问的。

Function.prototype.bind 被调用时,如果 this (即 func) 已经是一个绑定函数(即它拥有 [[BoundTargetFunction]] 等内部插槽),那么:

  1. thisArg (当前 bind 传入的 context) 将被忽略。
  2. 新的绑定函数的 [[BoundThis]] 将继承 func[[BoundThis]]
  3. 新的绑定函数的 [[BoundTargetFunction]] 将继承 func[[BoundTargetFunction]]
  4. 新的绑定函数的 [[BoundArguments]] 将是 func[[BoundArguments]] 与当前 bind 传入的 args1 的拼接。

如何模拟这些内部插槽?

我们不能直接访问或创建原生内部插槽。但我们可以在我们自己实现的 myBind 返回的函数上附加一些非标准属性(例如使用 Symbol 或不可枚举属性)来模拟这种行为。

改进方案:

  1. 标识绑定函数:fBound 上设置一个特殊属性,表明它是由 myBind 创建的绑定函数。
  2. 存储绑定信息:fBound 上存储 [[BoundTargetFunction]][[BoundThis]][[BoundArguments]] 的模拟值。
  3. 递归处理:myBind 内部,如果 this (原始函数 func) 本身就是一个由 myBind 创建的绑定函数,我们就递归地从它身上提取这些绑定信息,并将其合并到新的 fBound 中。
// 使用 Symbol 来创建独一无二的属性键,避免命名冲突
const IS_MY_BOUND_FUNCTION = Symbol('IS_MY_BOUND_FUNCTION');
const MY_BOUND_TARGET_FUNCTION = Symbol('MY_BOUND_TARGET_FUNCTION');
const MY_BOUND_THIS = Symbol('MY_BOUND_THIS');
const MY_BOUND_ARGUMENTS = Symbol('MY_BOUND_ARGUMENTS');

Function.prototype.myBind = function (context, ...args1) {
    let func = this; // 原始函数
    let finalContext = context;
    let finalArgs = args1;

    // 1. 如果 func 不是一个函数,抛出 TypeError
    if (typeof func !== 'function') {
        throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }

    // 2. 处理多层绑定:如果 func 本身就是一个由 myBind 创建的绑定函数
    //    我们需要从它身上提取原始的 targetFunction, boundThis, boundArguments
    if (func[IS_MY_BOUND_FUNCTION]) {
        // 原始的 targetFunction
        func = func[MY_BOUND_TARGET_FUNCTION];
        // 原始的 this (新的 thisArg 被忽略)
        finalContext = func[MY_BOUND_THIS]; // 注意这里是 func[MY_BOUND_THIS],而不是 context
        // 累加参数:原始绑定函数的参数 + 当前 bind 传入的参数
        finalArgs = func[MY_BOUND_ARGUMENTS].concat(args1);
    }

    const fBound = function (...args2) {
        // 3. 核心:判断是否通过 new 调用
        // 如果当前 this 是 fBound 的实例,说明是通过 new 调用的
        // 此时应该使用 this (新创建的实例) 作为上下文,否则使用 finalContext
        const actualContext = this instanceof fBound ? this : finalContext;

        // 合并所有参数 (原始绑定参数 + 当前 bind 参数 + 调用时参数)
        return func.apply(actualContext, finalArgs.concat(args2));
    };

    // 4. 维护原型链:
    // 创建一个继承自 func.prototype 的新对象,并将其赋给 fBound.prototype
    if (func.prototype) {
        fBound.prototype = Object.create(func.prototype);
    }

    // 5. 标记 fBound 为一个 myBind 创建的绑定函数,并存储其内部状态
    Object.defineProperty(fBound, IS_MY_BOUND_FUNCTION, { value: true });
    Object.defineProperty(fBound, MY_BOUND_TARGET_FUNCTION, { value: func });
    Object.defineProperty(fBound, MY_BOUND_THIS, { value: finalContext });
    Object.defineProperty(fBound, MY_BOUND_ARGUMENTS, { value: finalArgs });

    // 6. 模拟 length 和 name 属性 (可选,但为了更接近原生实现)
    // length 属性表示函数期望的参数数量,对于绑定函数,是原始函数的 length 减去已绑定的参数数量
    Object.defineProperty(fBound, 'length', {
        value: Math.max(0, func.length - finalArgs.length),
        writable: false,
        enumerable: false,
        configurable: true
    });
    // name 属性通常是 'bound ' + 原始函数的 name
    Object.defineProperty(fBound, 'name', {
        value: `bound ${func.name || ''}`,
        writable: false,
        enumerable: false,
        configurable: true
    });

    return fBound;
};

代码解析:

  1. Symbol 键: 我们使用 Symbol 来创建几个私有(但不是真正私有,只是难以访问)的属性键,用于存储绑定函数的状态。这样可以避免与用户或其他库可能设置的同名属性冲突。
  2. 多层绑定检测:
    • if (func[IS_MY_BOUND_FUNCTION]): 检查当前的 func (即 myBind 被调用的目标函数) 是否已经是一个由 myBind 创建的绑定函数。
    • 如果是,我们就从 func 身上提取它最初的目标函数 (func[MY_BOUND_TARGET_FUNCTION])、最初的 this (func[MY_BOUND_THIS]) 和已经绑定的参数 (func[MY_BOUND_ARGUMENTS])。
    • finalContext 会被设置为 func[MY_BOUND_THIS],这意味着第二次 bind 传入的 context 被忽略。
    • finalArgs 会累加:func[MY_BOUND_ARGUMENTS].concat(args1)
    • func 被更新为最原始的目标函数。
  3. fBound 内部逻辑: actualContext 的判断逻辑与之前相同,优先 new 提供的 this,否则使用 finalContext。参数合并使用 finalArgs.concat(args2)
  4. 标记与存储: 在创建 fBound 后,我们使用 Object.definePropertyIS_MY_BOUND_FUNCTION 标记为 true,并存储 MY_BOUND_TARGET_FUNCTIONMY_BOUND_THISMY_BOUND_ARGUMENTS。这些属性都是不可枚举的,更接近原生行为。
  5. lengthname 属性: 原生 bind 返回的函数也会调整其 lengthname 属性。
    • length 是原始函数的 length 减去已绑定的参数数量,且不能小于 0。
    • name 通常是 bound 加上原始函数的 name

测试多层绑定与综合功能:

// --- 原始函数 ---
function greetPerson(prefix, name, suffix) {
    return `${prefix} ${this.salutation} ${name}${suffix}`;
}
greetPerson.length = 3; // 确保 length 属性正确

const context1 = { salutation: "Mr." };
const context2 = { salutation: "Ms." };

// --- 基础绑定 ---
const boundGreetMr = greetPerson.myBind(context1, "Hello");
console.log("Basic Bind:", boundGreetMr("John", "!")); // 期望: Hello Mr. John!
console.log("boundGreetMr.length:", boundGreetMr.length); // 期望: Math.max(0, 3 - 1) = 2
console.log("boundGreetMr.name:", boundGreetMr.name); // 期望: bound greetPerson

// --- 多层绑定 (this 不变,参数累加) ---
const boundGreetMrHelloJohn = boundGreetMr.myBind(context2, "John"); // context2 应该被忽略
console.log("Multi-level Bind:", boundGreetMrHelloJohn("!")); // 期望: Hello Mr. John!
console.log("boundGreetMrHelloJohn.length:", boundGreetMrHelloJohn.length); // 期望: Math.max(0, 3 - 2) = 1
console.log("boundGreetMrHelloJohn.name:", boundGreetMrHelloJohn.name); // 期望: bound greetPerson

// --- 再次多层绑定 (this 仍不变,参数累加) ---
const boundGreetMrHelloJohnExclaim = boundGreetMrHelloJohn.myBind(null, "!!!");
console.log("Multi-level Bind 2:", boundGreetMrHelloJohnExclaim()); // 期望: Hello Mr. John!!!
console.log("boundGreetMrHelloJohnExclaim.length:", boundGreetMrHelloJohnExclaim.length); // 期望: Math.max(0, 3 - 3) = 0

// --- new 操作符与多层绑定 ---
function Developer(name, language) {
    this.name = name;
    this.language = language;
}
Developer.prototype.code = function () {
    return `${this.name} codes in ${this.language}`;
};

const JavaDeveloper = Developer.myBind(null, "Java");
const SeniorJavaDeveloper = JavaDeveloper.myBind(null, "Senior"); // 参数累加: ["Senior", "Java"]
const Alice = new SeniorJavaDeveloper("Alice"); // new 操作符忽略 bind 的 this
console.log("New with Multi-level Bind (Alice):", Alice.code()); // 期望: Senior codes in Java (参数倒序了,需要调整)
console.log("Alice instanceof Developer:", Alice instanceof Developer); // 期望: true

// 重新审视 `Developer` 的绑定
// `Developer.myBind(null, "Java")` -> fBound_Java (target=Developer, this=null, args=["Java"])
// `fBound_Java.myBind(null, "Senior")` -> fBound_Senior (target=Developer, this=null, args=["Java", "Senior"])
// `new fBound_Senior("Alice")` -> actualContext=newObject, func=Developer, finalArgs=["Java", "Senior"], args2=["Alice"]
// `Developer.apply(newObject, ["Java", "Senior", "Alice"])`
// `Developer(name, language)` -> this.name = "Java", this.language = "Senior" (错误)
// 这里的参数处理逻辑需要调整:`finalArgs` 应该作为 *前置* 参数,`args2` 才是 `new` 调用的构造函数参数。

// 正确的参数传递顺序是:bind 预设参数,然后是 new 传入的参数。
// 假设 Developer 构造函数是 (language, name)
function DeveloperCorrected(language, name) {
    this.name = name;
    this.language = language;
}
DeveloperCorrected.prototype.code = function () {
    return `${this.name} codes in ${this.language}`;
};

const JavaDeveloperCorrected = DeveloperCorrected.myBind(null, "Java"); // 绑定 language 为 "Java"
const AliceDev = new JavaDeveloperCorrected("Alice"); // 传入 name 为 "Alice"
console.log("New with Bind (AliceDev):", AliceDev.code()); // 期望: Alice codes in Java
console.log("AliceDev instanceof DeveloperCorrected:", AliceDev instanceof DeveloperCorrected); // 期望: true

// 再次多层绑定
const SeniorJavaDeveloperCorrected = JavaDeveloperCorrected.myBind(null, "Senior"); // 这里绑定的是第二个参数 (name),所以是 "Senior"
const BobDev = new SeniorJavaDeveloperCorrected("Bob"); // 这里传入的参数是第三个参数 (会溢出)

// 这说明参数累加的逻辑是正确的,但理解和使用多层绑定时,需要非常清楚原始函数的参数顺序。
// 原生 bind 确实是简单地将参数累加,而不关心其在原始函数中的语义。
// 这也提醒我们,多层 bind 很少用于绑定不同的位置参数,更多是用于固定前几个参数。

关于箭头函数的特殊情况:

箭头函数没有自己的 this 绑定,也没有 prototype 属性,因此它们不能作为构造函数使用,也不能通过 bind 改变它们的 this。尝试对箭头函数进行 bindthisArg 会被忽略,它会继续使用词法作用域的 this。我们的 myBind 在处理 func.prototype 时,如果 func 是箭头函数,func.prototype 会是 undefined,所以 fBound.prototype = Object.create(func.prototype) 这行会正常跳过,这符合预期。this instanceof fBound 仍然可以工作,但 new 箭头函数本身就会抛出错误。

最终的 Function.prototype.myBind 实现(带详细注释)

将所有考虑到的复杂性整合到一个最终的、鲁棒的 myBind 实现中。

/**
 * 手写实现 Function.prototype.bind
 * 处理复杂兼容性:包括 new 操作符、多层参数绑定、length/name 属性
 *
 * @param {object} context - 绑定函数的 this 上下文
 * @param {...any} args1 - 绑定时预设的参数
 * @returns {Function} - 新的绑定函数
 */
Function.prototype.myBind = function (context, ...args1) {
    // 1. 获取原始函数 (即调用 myBind 的函数)
    let func = this;

    // 2. 类型检查:确保被绑定的对象是一个可调用函数
    if (typeof func !== 'function') {
        throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }

    // 3. 定义内部状态的 Symbol 键,模拟内部插槽,避免命名冲突
    // 这些属性用于在多层绑定时,获取前一个绑定函数的原始信息
    const IS_MY_BOUND_FUNCTION_KEY = Symbol('[[IsMyBoundFunction]]');
    const MY_BOUND_TARGET_FUNCTION_KEY = Symbol('[[MyBoundTargetFunction]]');
    const MY_BOUND_THIS_KEY = Symbol('[[MyBoundThis]]');
    const MY_BOUND_ARGUMENTS_KEY = Symbol('[[MyBoundArguments]]');

    // 4. 初始化最终的 this 上下文和参数列表
    // 它们可能在处理多层绑定时被修改
    let finalContext = context;
    let finalArgs = args1;
    let originalTargetFunction = func; // 存储最原始的目标函数,用于原型链和 length/name

    // 5. 处理多层绑定:如果 func 本身已经是一个由 myBind 创建的绑定函数
    if (func[IS_MY_BOUND_FUNCTION_KEY]) {
        // 获取前一个绑定函数的原始目标函数
        originalTargetFunction = func[MY_BOUND_TARGET_FUNCTION_KEY];
        // 忽略当前 bind 传入的 context,沿用前一个绑定函数的 this
        finalContext = func[MY_BOUND_THIS_KEY];
        // 累加参数:前一个绑定函数的参数 + 当前 bind 传入的参数
        finalArgs = func[MY_BOUND_ARGUMENTS_KEY].concat(args1);
        // 更新 func 为最原始的目标函数,以便后续调用
        func = originalTargetFunction;
    }

    // 6. 定义返回的绑定函数
    const fBound = function (...args2) {
        // 7. 处理 new 操作符:
        //   - 如果绑定函数被 new 调用 (即 this 是 fBound 的实例),
        //     则忽略 bind 传入的 context,使用 new 创建的新实例作为 this。
        //   - 否则,使用 bind 传入或多层绑定决定的 finalContext 作为 this。
        const actualContext = this instanceof fBound ? this : finalContext;

        // 8. 调用原始函数 func:
        //   - 使用 actualContext 作为 this。
        //   - 参数是 finalArgs (bind 预设或累加的参数) 加上 args2 (调用 fBound 时传入的参数)。
        return func.apply(actualContext, finalArgs.concat(args2));
    };

    // 9. 维护原型链:
    //   - 如果原始函数 func 拥有 prototype (普通函数有,箭头函数没有),
    //     则将 fBound 的 prototype 链接到 func 的 prototype。
    //   - 这样,通过 new fBound() 创建的实例就能正确继承 func 的原型链。
    //   - 使用 Object.create() 创建一个新对象作为 fBound 的 prototype,
    //     避免直接修改 func.prototype。
    if (originalTargetFunction.prototype) {
        fBound.prototype = Object.create(originalTargetFunction.prototype);
    }

    // 10. 存储绑定函数的内部状态 (模拟内部插槽)
    // 这些属性是不可枚举的,更接近原生 bind 的行为
    Object.defineProperty(fBound, IS_MY_BOUND_FUNCTION_KEY, { value: true, configurable: true });
    Object.defineProperty(fBound, MY_BOUND_TARGET_FUNCTION_KEY, { value: originalTargetFunction, configurable: true });
    Object.defineProperty(fBound, MY_BOUND_THIS_KEY, { value: finalContext, configurable: true });
    Object.defineProperty(fBound, MY_BOUND_ARGUMENTS_KEY, { value: finalArgs, configurable: true });

    // 11. 模拟原生 bind 函数的 length 属性
    //   - length 表示函数期望接收的未绑定参数数量。
    //   - 它是原始函数的 length 减去已绑定的参数数量,且不能小于 0。
    Object.defineProperty(fBound, 'length', {
        value: Math.max(0, originalTargetFunction.length - finalArgs.length),
        writable: false,
        enumerable: false,
        configurable: true
    });

    // 12. 模拟原生 bind 函数的 name 属性
    //   - 通常是 'bound ' 加上原始函数的 name。
    //   - 如果原始函数没有 name (如匿名函数),则为空字符串。
    Object.defineProperty(fBound, 'name', {
        value: `bound ${originalTargetFunction.name || ''}`,
        writable: false,
        enumerable: false,
        configurable: true
    });

    // 13. 返回新创建的绑定函数
    return fBound;
};

测试用例表格:

场景 描述 预期结果
基础功能 func.myBind(context, arg1) this 绑定到 contextarg1 作为前置参数
参数部分应用 func.myBind(context, arg1, arg2)(arg3) this 绑定,参数合并为 [arg1, arg2, arg3]
new 操作符 new func.myBind(context, arg1)() context 被忽略,this 指向新创建的实例,实例继承 func.prototype
new func.myBind(null, arg1)() 同上,null 被忽略
多层绑定 (this) func.myBind(ctx1, a1).myBind(ctx2, a2) this 绑定到 ctx1 (第一次绑定生效),ctx2 被忽略
多层绑定 (args) func.myBind(ctx, a1).myBind(null, a2) this 绑定到 ctx,参数累加为 [a1, a2]
new + 多层 new func.myBind(ctx1, a1).myBind(ctx2, a2)() this 指向新实例,参数累加为 [a1, a2],实例继承 func.prototype
length 属性 func.myBind(ctx, a1).length Math.max(0, func.length - 1)
name 属性 func.myBind(ctx).name bound func.name
非函数绑定 ({}).myBind(...) 抛出 TypeError
箭头函数 const arrow = () => this; arrow.myBind(obj) this 绑定无效,仍使用词法作用域 thisprototype 继承跳过,new 仍然报错

总结

通过这次深度实践,我们一步步地构建了一个高度兼容 Function.prototype.bind 的手写实现。我们从基础的 this 绑定和参数预设开始,逐步引入 new 操作符的特殊行为处理,最终攻克了多层参数绑定的复杂逻辑。这个过程不仅加深了我们对 bind 内部机制的理解,也揭示了 JavaScript 运行时中函数调用、this 上下文、原型链以及内部插槽等核心概念的精妙之处。

理解并能够手写这样的复杂兼容性函数,是衡量一个 JavaScript 开发者深厚功底的重要标志。希望这次讲座能为大家带来启发,让大家在日常开发中对 bind 及其背后的原理有更清晰的认识。

发表回复

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