各位编程领域的同仁们,大家好!
今天,我们将深入探讨 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 的两个核心功能:
- 永久绑定
this上下文。 - 实现函数的部分应用(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));
};
};
代码解析:
const func = this;: 在myBind被调用时,this指向调用myBind的那个函数(即原始函数)。我们将其保存到func变量中。...args1: 这是bind方法接收到的除context之外的所有参数,它们将被预先绑定。return function (...args2) { ... };:bind总是返回一个新的函数。当这个新函数被调用时,它可能接收到新的参数args2。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 调用时,会发生以下几个步骤:
- 创建一个新的空对象。
- 将这个新对象的
[[Prototype]]链接到构造函数的prototype属性。 - 将构造函数的作用域赋给新的
this上下文。 也就是说,在构造函数内部,this会指向这个新创建的对象。 - 执行构造函数。 构造函数可能会修改
this对象。 - 如果构造函数没有显式返回一个对象,则默认返回新创建的
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 的实例。
bind 的 new 兼容性规则:
当绑定函数作为构造函数(使用 new)被调用时:
bind传入的thisArg将被完全忽略。this上下文会指向new操作符创建的新实例。- 绑定函数会像普通构造函数一样执行,并且其
prototype链也会被正确维护。
第二次尝试:处理 new 操作符
为了实现 new 兼容性,我们需要解决两个核心问题:
- 在绑定函数被
new调用时,忽略bind传入的thisArg,并使用new提供的this。 - 确保绑定函数的实例能够正确地继承原始函数的
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):
- 创建一个空函数
fNOP。 - 将
fNOP.prototype设置为原始函数func.prototype。 - 将
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 的规则是:
this绑定是单向的,不可覆盖。 如果一个函数已经被bind绑定了this,那么对它再次调用bind时,新的thisArg参数会被忽略,它将保持第一次绑定时的this。- 参数绑定是累加的。 每次调用
bind都会在之前绑定的参数基础上,追加新的预设参数。
这意味着,当 log.bind(null, "WARN") 返回一个绑定函数 warn 后,warn 内部会记住它是由 log 绑定而来,且其 this 绑定是 null,第一个参数是 "WARN"。
当 warn.bind(null, "ERROR") 被调用时,它会:
- 忽略新的
thisArg(null),因为它已经从log.bind继承了null的this绑定。 - 累加参数:在
["WARN"]的基础上,追加["ERROR"],最终参数列表变为["WARN", "ERROR"]。
显然,这与我们期望的 [ERROR] Something failed 不符。原生 bind 的行为是,如果一个函数已经是绑定函数,那么对其再次 bind 时,它会获取其内部 [[BoundTargetFunction]](原始目标函数)、[[BoundThis]] 和 [[BoundArguments]],然后用新的参数扩充 [[BoundArguments]],但 [[BoundThis]] 保持不变。
因此,log.bind(null, "WARN").bind(null, "ERROR") 最终会绑定 log 到 null,参数为 ["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]] 等内部插槽),那么:
thisArg(当前bind传入的context) 将被忽略。- 新的绑定函数的
[[BoundThis]]将继承func的[[BoundThis]]。 - 新的绑定函数的
[[BoundTargetFunction]]将继承func的[[BoundTargetFunction]]。 - 新的绑定函数的
[[BoundArguments]]将是func的[[BoundArguments]]与当前bind传入的args1的拼接。
如何模拟这些内部插槽?
我们不能直接访问或创建原生内部插槽。但我们可以在我们自己实现的 myBind 返回的函数上附加一些非标准属性(例如使用 Symbol 或不可枚举属性)来模拟这种行为。
改进方案:
- 标识绑定函数: 在
fBound上设置一个特殊属性,表明它是由myBind创建的绑定函数。 - 存储绑定信息: 在
fBound上存储[[BoundTargetFunction]]、[[BoundThis]]和[[BoundArguments]]的模拟值。 - 递归处理: 在
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;
};
代码解析:
- Symbol 键: 我们使用
Symbol来创建几个私有(但不是真正私有,只是难以访问)的属性键,用于存储绑定函数的状态。这样可以避免与用户或其他库可能设置的同名属性冲突。 - 多层绑定检测:
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被更新为最原始的目标函数。
fBound内部逻辑:actualContext的判断逻辑与之前相同,优先new提供的this,否则使用finalContext。参数合并使用finalArgs.concat(args2)。- 标记与存储: 在创建
fBound后,我们使用Object.defineProperty将IS_MY_BOUND_FUNCTION标记为true,并存储MY_BOUND_TARGET_FUNCTION、MY_BOUND_THIS、MY_BOUND_ARGUMENTS。这些属性都是不可枚举的,更接近原生行为。 length和name属性: 原生bind返回的函数也会调整其length和name属性。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。尝试对箭头函数进行 bind,thisArg 会被忽略,它会继续使用词法作用域的 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 绑定到 context,arg1 作为前置参数 |
| 参数部分应用 | 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 绑定无效,仍使用词法作用域 this;prototype 继承跳过,new 仍然报错 |
总结
通过这次深度实践,我们一步步地构建了一个高度兼容 Function.prototype.bind 的手写实现。我们从基础的 this 绑定和参数预设开始,逐步引入 new 操作符的特殊行为处理,最终攻克了多层参数绑定的复杂逻辑。这个过程不仅加深了我们对 bind 内部机制的理解,也揭示了 JavaScript 运行时中函数调用、this 上下文、原型链以及内部插槽等核心概念的精妙之处。
理解并能够手写这样的复杂兼容性函数,是衡量一个 JavaScript 开发者深厚功底的重要标志。希望这次讲座能为大家带来启发,让大家在日常开发中对 bind 及其背后的原理有更清晰的认识。