实现 bind 方法
基础知识
在实现我们自己的 bind
方法之前,需要先掌握以下几个 JavaScript 核心概念:
this
关键字: 在 JavaScript 中,this
的值取决于函数的调用方式。- 在全局作用域中,
this
指向全局对象 (window
或global
)。 - 作为对象方法调用时,
this
指向该对象。 - 使用
call
、apply
或bind
调用时,this
指向这些方法指定的第一个参数。 - 作为构造函数使用
new
调用时,this
指向新创建的实例对象。 - 在箭头函数中,
this
继承自其父级作用域。
- 在全局作用域中,
Function.prototype.call()
:call()
方法使用一个指定的this
值和单独给出的一个或多个参数来调用一个函数。func.call(thisArg, arg1, arg2, ...);
Function.prototype.apply()
:apply()
方法调用一个具有给定this
值的函数,以及以一个数组(或类数组对象)的形式提供的参数。func.apply(thisArg, [argsArray]);
闭包 (Closure): 闭包是指有权访问另一个函数作用域中变量的函数。在实现
bind
时,我们需要利用闭包来保存指定的this
值和预设的参数。函数柯里化 (Currying): 这是一种将接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
bind
的实现就运用了柯里化的思想。
核心思路
我们的目标是创建一个 Function.prototype.myBind
方法,它应该能像原生的 bind
方法一样工作。其核心思路如下:
myBind
方法应该挂载在Function.prototype
上,这样所有的函数实例都可以调用它。- 它应该接受至少一个参数
context
,用来指定新函数执行时的this
指向。context
后面的所有参数将作为预设参数。 myBind
必须返回一个新的函数。这是bind
的核心功能——它并不立即执行,而是返回一个绑定了上下文的新函数。- 返回的新函数在被调用时,应该使用我们传入的
context
作为其this
值。我们可以使用apply
或call
来实现这一点。 - 返回的新函数在被调用时,可以接收额外的参数。这些参数需要和
myBind
时传入的预设参数合并在一起,并传递给原始函数。 - 一个关键点是:如果返回的绑定函数被用作构造函数(即通过
new
操作符调用),那么bind
所指定的this
值将被忽略,而this
将指向新创建的实例。
关键点
根据核心思路,我们在实现时需要注意以下几个关键点:
- 获取调用者: 在
myBind
内部,需要获取到调用myBind
的原始函数。因为myBind
是作为函数方法调用的,所以this
就指向了这个原始函数。 - 参数合并: 需要将
myBind
时传入的预设参数和返回的新函数被调用时传入的参数合并起来。可以使用Array.prototype.slice
和Array.prototype.concat
来处理参数。 - 处理
new
操作符: 这是实现一个健壮的bind
最容易被忽略的一点。当绑定函数被new
调用时,new
的优先级更高。我们需要判断返回的函数是否被用作构造函数。可以通过instanceof
来判断。如果被new
调用,this
应该指向new
创建的新对象,而不是我们bind
时指定的context
。
代码实现
下面是 myBind
的一个完整实现,包含了对上述所有关键点的处理。
/**
* 自定义 bind 方法
*
* @param {Object} context - 新函数执行时绑定的 this 对象
* @param {...*} args - 预设的参数列表
* @returns {Function} - 返回一个绑定了上下文和预设参数的新函数
*/
Function.prototype.myBind = function (context) {
// 1. 检查调用者是否为函数,如果不是则抛出错误
if (typeof this !== 'function') {
throw new TypeError(
'Function.prototype.bind - what is trying to be bound is not callable'
);
}
// 2. 保存调用 myBind 的原始函数(`this`)和预设参数
var self = this; // self 指向原始函数
var args = Array.prototype.slice.call(arguments, 1); // 获取 myBind 时传入的除 context 外的参数
// 3. 创建一个中间函数 F,用于处理 `new` 操作符
// 这样做是为了让返回的函数 fBound 的原型链能够连接到原始函数的原型链
var F = function () {};
// 4. 返回一个新的函数 fBound
var fBound = function () {
// 5. 合并预设参数和调用时传入的新参数
var bindArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(bindArgs);
// 6. 判断 fBound 是否被用作构造函数(通过 `new` 调用)
// 如果是,`this` 应该指向 new 创建的实例 (this instanceof F)
// 如果不是,`this` 应该指向 myBind 时传入的 context
return self.apply(this instanceof F ? this : context, finalArgs);
};
// 7. 设置原型链,使得 fBound 的实例能够继承原始函数的原型属性
// F.prototype = this.prototype;
// fBound.prototype = new F();
// 更健壮的写法是使用 Object.create,避免直接修改 F.prototype 带来的副作用
if (this.prototype) {
F.prototype = this.prototype;
}
fBound.prototype = new F();
return fBound;
};
代码解析
- if (typeof this !== 'function'):首先确保调用
myBind
的是一个函数。 - var self = this;:在
myBind
内部,this
指向调用它的函数(例如myFunction.myBind(...)
中的myFunction
)。用self
变量保存它,以便在返回的函数fBound
中使用。 - var args = Array.prototype.slice.call(arguments, 1);:
arguments
是包含所有传递给myBind
参数的类数组对象。- 使用
slice
从第二个参数开始(跳过第一个参数context
)提取所有预设参数。
- var fBound = function() { ... };:
- 这是返回的核心函数。
- 闭包使
fBound
能够访问外部作用域的self
、context
和args
。
- var finalArgs = args.concat(bindArgs);:将
myBind
时预设的args
和fBound
被调用时传入的bindArgs
合并成最终的参数列表。 - this instanceof F ? this : context:处理
new
操作符的关键逻辑:- new fBound():当使用
new
调用时,fBound
内部的this
指向新创建的对象,该对象原型指向fBound.prototype
。由于设置了fBound.prototype = new F()
,且F.prototype
指向原始函数原型,因此this instanceof F
为true
,apply
的第一个参数为新创建的实例this
。 - boundFunc():作为普通函数调用时,
this
默认为全局对象(严格模式下为undefined
),this instanceof F
为false
,apply
的第一个参数为bind
时指定的context
。
- new fBound():当使用
- fBound.prototype = new F();:
- 让
fBound
的原型链与原始函数self
的原型链连接。 - 这样,当
new fBound()
创建实例时,实例可通过原型链访问self.prototype
上的属性和方法。例如,若Person
是构造函数,boundPerson = Person.bind(...)
,则new boundPerson()
是Person
的实例。
- 让