一、bind 方法介绍
- bind 方法创建一个新的绑定函数
- bind 方法重新绑定原函数的 this 值
- 在调用绑定函数时,将bind 中的给定参数作为原函数的参数
function.bind(thisArg, arg1, arg2, ...)
- thisArg
调用绑定函数时,将原函数的 this 绑定为 thisArg
- 如果将绑定函数作为构造函数,通过关键字
new
调用,则忽略参数 thisArg
二、从简单到复杂
版本 1: apply 模拟 bind
版本一不考虑那么多,仅仅使用 apply 来实现一个简单的 bind。调用语法和 bind 相似:
function.myBind(context, arg1, arg2, ...)
这里核心就是在 myBind 函数中如何获取原函数?
利用 this 关键字即可达到目的,具体请看 梳理 this 关键字的指向 中的对象方法
。
Function.prototype.myBind = function (context) {
const fToBind = this; // 此处 this 指向调用 myBind 函数的对象,其实就是原函数。
// 新的绑定函数
function fBound() {
// 说明:这里返回的是原函数执行 apply 后返回的结果
return fToBind.apply(context);
}
// 返回绑定函数
return fBound;
}
调用示例:
demo 版本一版本 2: 考虑参数
这里参数有两种情况:
- myBind 的预设参数
- 新的绑定函数 fBound 的参数
Function.prototype.myBind = function (context) {
const fToBind = this; // 此处 this 指向调用 myBind 函数的对象,其实就是原函数。
const outerArgs = [].slice.call(arguments, 1);
// 返回新的绑定函数
function fBound () {
const innerArgs = [].slice.call(arguments);
// 说明:这里返回的是原函数执行 apply 后返回的结果
return fToBind.apply(context, outerArgs.concat(innerArgs));
}
// 返回绑定函数
return fBound;
}
调用示例:
在例子中利用 myBind 实现了偏函数,新的绑定函数 add
在调用时,只传入了一个参数 2, 但是结果是 1 + 2
。
版本 3: 考虑构造函数的影响
3.1 如果将绑定函数作为构造函数,通过关键字 new
调用,则忽略参数 context。
通过 instanceof
即可判断是否通过 new 关键字调用构造函数。
Function.prototype.myBind = function (context) {
const fToBind = this; // 此处 this 指向调用 myBind 函数的对象,其实就是原函数。
const outerArgs = [].slice.call(arguments, 1);
// 返回新的绑定函数
function fBound () {
const innerArgs = [].slice.call(arguments);
// 说明:这里返回的是原函数执行 apply 后返回的结果
return fToBind.apply(this instanceof fBound ? this : context, outerArgs.concat(innerArgs));
}
// 返回绑定函数
return fBound;
}
运行示例:
从以下例子中可以看出,Person 是通过 myBind 生成的新的绑定函数:
- Person 作为构造函数,通过 new 关键字调用时,this 指向实例对象,obj 被忽略了。
- Person 作为普通函数调用时, this 指向 obj。
3.2 原型链的影响
上述的仍旧不够完美,Person 当作构造函数时,如果在函数 person 中执行语句 this.say()
,就会报错。
因为 myBind 函数返回的绑定函数 fBound 的 prototype 和原函数的 prototype 并不相同。
那能不能直接简单粗暴地执行语句fBound.prototype = fToBind.prototype
,将原函数的 prototype 赋值给 fBound 呢?
很明显这样的操作把 fBound 和 原函数的 prototype 强关联起来了,如果 fBound 函数的 prototype 将会影响到原函数的 prototype。
所以可以联想到通过 fBound.prototype = Object.create(fToBind.prototype)
,以原函数的 prototype 为模板,生成一个新的实例对象,并赋值给 fBound.prototype。
Function.prototype.myBind = function (context) {
const fToBind = this; // 此处 this 指向调用 myBind 函数的对象,其实就是原函数。
const outerArgs = [].slice.call(arguments, 1);
// 返回新的绑定函数
function fBound () {
const innerArgs = [].slice.call(arguments);
// 说明:这里返回的是原函数执行 apply 后返回的结果
return fToBind.apply(this instanceof fBound ? this : context, outerArgs.concat(innerArgs));
}
fBound.prototype = Object.create(fToBind.prototype);
// 返回绑定函数
return fBound;
}
运行示例:
从以下例子中可以发现,eat 函数之后被添加到 fBound 的原型上,而不会影响到原函数。
3.3 mdn 中并未使用 Object.create()
因为 Object.create()
和 bind
都是 ES5
规范提出的,如果不支持 bind, 那么bind 的 polyfill 里面自然不支持 Object.create()。
Object.create 的实现:
原型式继承:(参见 js 高级程序设计)
function object(o) {
function F() {};
F.prototype = o;
return new F();
}
应用到 myBind 中:
Function.prototype.myBind = function (context) {
const fToBind = this; // 此处 this 指向调用 myBind 函数的对象,其实就是原函数。
const fNop = function () {};
const outerArgs = [].slice.call(arguments, 1);
// 返回新的绑定函数
function fBound () {
const innerArgs = [].slice.call(arguments);
// 说明:这里返回的是原函数执行 apply 后返回的结果
return fToBind.apply(this instanceof fBound ? this : context, outerArgs.concat(innerArgs));
}
// 这里为什么需要判断?
if (fToBind.prototype) {
fNop.prototype = fToBind.prototype;
}
fBound.prototype = new fNop();
// 返回绑定函数
return fBound;
}
疑惑:为什么需要对 fToBind.prototype
进行判断?
因为 Function.prototype
是一个函数,而且它没有 prototype。
即防止有人这么调用 bind 方法: Function.prototype.bind()
3.4 instanceof 不准确
这个使用 new.target
即可。
参考
MDN bind
从一道面试题的进阶,到“我可能看了假源码”
stackoverflow
MDN Object.create
更深入的分析文章推荐
从一道面试题的进阶,到“我可能看了假源码”(2)
面试官问:能否模拟实现JS的bind方法
不用call和apply方法模拟实现ES5的bind方法
网友评论