「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」
0. 相关概念📜
开门见山,首先我们先看看 bind 函数做了什么:
MDN:bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。
由此我们可以首先得出 bind 函数的两个特点:
- 返回一个函数,并指定其实际调用者
this。 - 绑定时可以传入参数。
了解了 bind 函数的相关逻辑后,我们可以尝试着手写实现,首先应关注的是 指定 this 这个功能,对此我们可以通过 apply 函数为其指定。于是我们有了如下的代码👇
1. 手写实现✍
① 初版代码:指定 this
Function.prototype.myBind = function (context) {
// 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 bar
const self = this;
return function () {
// 利用 apply 指定传入的 context 作为 this
// 同时返回出去,因为原函数 *可能有返回值*
return self.apply(context);
};
};
// 使用
let foo = {
value: 1,
};
function bar() {
console.log(this.value);
}
function test() {
return this.value; // 可能有返回值
}
// 调用 myBind,传入的 context 为 foo,并且返回一个函数
let bindFoo1 = bar.myBind(foo);
let bindFoo2 = test.myBind(foo);
bindFoo1(); // 1
console.log("bindFoo2:", bindFoo2()); // 存在返回值的情况
初步代码初步实现了 this 的绑定,但考虑不周:因为原生 bind 函数是可以传参的,并且可以分两步走,即绑定时可以传参,后续调用时也可以传参。 即如下代码:
let foo = {
value: 1
};
function bar(name, age) {
console.log(this.value);
console.log(name);
console.log(age);
}
// 绑定时传参
let bindFoo = bar.bind(foo, 'John');
// 调用时传参
bindFoo('13');
对此,我们可以为传入的参数做一次合并,这就要使用到 Arguments 对象了,它可以轻松地取得我们调用函数时传入的参数。
② 二版代码:实现传参
Function.prototype.myBind = function (context) {
// 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 bar
const self = this;
// 获取 myBind 函数从第二个参数到最后一个参数(第一个参数是 context)
// 这里产生了闭包~
const args = Array.prototype.slice.call(arguments, 1);
return function () {
// 这个时候的 arguments 是指 myBind 返回的函数传入的参数,我全都要!
const bindArgs = [...arguments]; // 可以采用 ES2015 的语法将其转为数组
// 合并
return self.apply(context, args.concat(bindArgs));
};
};
第二版代码已经初步实现了 bind 函数,但仍有欠缺,因为原生 bind 函数还有一个用法:作为构造函数使用的绑定函数。对此,简单来说就是:一个调用了 bind 之后返回的函数,也能使用 new 操作符创建对象:这种行为就像把原函数当成构造器。这样做的话,原本绑定传入的 context 参数会被忽略,但传入的参数依然生效。如下例:
let value = 2; // 用于后续验证
let foo = {
value: 1
};
function bar(name, age) {
this.habit = 'eating';
console.log(this.value);
console.log(name);
console.log(age);
}
let bindFoo = bar.bind(foo, 'John');
let obj = new bindFoo('18');
// undefined // 原有的 this 失效
// John
// 18
console.log(obj.habit);
// eating
// 而使用我们自编的代码:
let myBindFoo = bar.myBind(foo)
let myObj = new myBindFoo("John","18")
// 1 // 原有的 this 未失效
// John
// 18
// 并且你可以尝试控制台输出:myObj 是空对象,而 habit 被挂到了 foo 对象上
由上述代码可见,使用原生 bind 生成绑定函数后,通过 new 操作符调用该函数时,this.value 是一个 undefined,既未输出 foo 对象内的 value,也未输出 window 对象的 value。实际上此时 this 指向的就是自身 obj,该现象符合正常调用 new 操作符时的表现。
而采用 myBind 生成绑定函数后,后续的操作都运用在了 foo 对象上。具体原因就像是使用 new 操作符执行该绑定函数时内部的 this 值未更改。简单来说,就是有 new 没 new 都一样!
那么问题出在哪?☹
其实就出在我们返回的是一个 self.apply(context) 函数,当其作为构造函数执行时,其 this 还是会被 apply 更改为 context,并非为新实例。
问题描述完毕,这个缺点我们可以通过增加一个调用者判定进行完善。
③ 三版代码:完善逻辑
Function.prototype.myBind = function (context) {
// 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 bar
const self = this;
const args = Array.prototype.slice.call(arguments, 1);
// fBound 指的是绑定函数,指的是基于 self 之上包装了一层的函数
const fBound = function () {
const bindArgs = [...arguments];
// 当绑定函数作为构造函数时,其内部的 this 应该指向实例,此时需要更改绑定函数的 this 为实例
// 当作为普通函数时,this 一般指向 window,此时判定结果为 false,将绑定函数的 this 指向 context 即可
// this instanceof fBound 的 this 就是绑定函数的调用者
return self.apply(
this instanceof fBound ? this : context,
args.concat(bindArgs)
);
};
return fBound;
};
// 使用
let foo = {
value: 1,
};
function bar(name) {
this.habit = "eating";
console.log(this.value);
console.log(name);
}
bar.prototype.testFn = function () {
console.log("yes!");
};
let bindFoo = bar.myBind(foo);
let obj = new bindFoo("John");
// undefined
// John
console.log(obj.habit); // eating
console.log(obj instanceof bindFoo); // true
console.log(obj instanceof bar); // false
obj.testFn(); // obj.testFn is not a function
// ??????????又有坑!
通过对绑定函数的调用者进行判定,我们成功地将 new 操作符调用绑定函数行为恢复正常,但也不算太正常,毕竟生成的新实例居然用不了 bar.prototype 上的方法!
bind 函数在本意上,只是对原函数做一层包装,为其指明原函数的调用者,而不应该破坏其原有的逻辑。但在此,我们本意应该是令新实例由 bar 构造而成,但却无法使用上它原型对象的方法!
针对此缺点,我们进行如下改进:
④ 四版代码:修复继承关系
该版代码的改进思路在于,将返回的绑定函数的原型对象的 __proto__ 属性,修改为原函数的原型对象。便可满足原有的继承关系。
Function.prototype.myBind = function (context) {
const self = this;
const args = Array.prototype.slice.call(arguments, 1);
const fBound = function () {
const bindArgs = [...arguments];
return self.apply(
this instanceof fBound ? this : context,
args.concat(bindArgs)
);
};
// 采用 Object.create 将绑定函数的 prototype.__proto__ 修改为 self.prototype
// 因为绑定函数在原函数之上做了一层封装,这样更改才可以正确实现继承。
fBound.prototype = Object.create(self.prototype);
return fBound;
};
// 使用
let foo = {
value: 1,
};
function bar(name) {
this.habit = "eating";
console.log(this.value);
console.log(name);
}
bar.prototype.testFn = function () {
console.log("yes!");
};
let bindFoo = bar.myBind(foo);
let obj = new bindFoo("John");
// undefined
// John
console.log(obj.habit); // eating
obj.testFn(); // yes!
console.log(obj instanceof bindFoo); // true
console.log(obj instanceof bar); // true
对此,我们已经基本实现了 bind 函数,不过仍然有些小瑕疵,便是关于 bind 函数的调用者的问题,调用 bind 函数的必须是一个函数!!基于此,我们可以完善出最终的代码!
⑤ 最终代码:添加校验
Function.prototype.myBind = function (context) {
if (typeof this !== "function") {
throw new Error(`${this}.bind is not a function`);
}
const self = this;
const args = Array.prototype.slice.call(arguments, 1);
const fBound = function () {
const bindArgs = [...arguments];
return self.apply(
this instanceof fBound ? this : context,
args.concat(bindArgs)
);
};
// ?
if (self.prototype) {
fBound.prototype = Object.create(self.prototype);
}
return fBound;
};
最后部分对 self.prototype 进行了判断,因为有一种函数是没有 prototype 属性的,那就是 Function.prototype 啦。
什么?你还不知道!可以去看看笔者之前写的文章!它其实是一个初始设计问题。
Finally!完结撒花!💐
2. 总结💬
在本文中,笔者详尽介绍了 bind 手写的思路,并且把每一个坑点都做了相关介绍以及代码的逻辑修复,相信看到这里的你一定可以收获满满!
若你对于 new 操作符或 Object.create() 等存有疑问:可以参考笔者之前的文章,对相关知识介绍得比较详尽,并且也对其做了手写实现,欢迎学习!
若你对 apply() 或是 call() 的实现感兴趣,可以期待一下我之后的文章~
很感谢你可以看到这里,也期待你可以留下一个印记👍这对笔者来说也是一种鼓励!
若文章出现了纰漏或者你有更好的建议,欢迎评论区留言或是私信指出✨