bind的描述
MDN对bind的描述
用f.bind(someObject)会创建一个与f具有相同函数体和作用域的函数, 但是在这个新函数中,this将永久地被绑定到了bind的第一个参数,无论这个函数是如何被调用的。
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function() {
return this.x + this.y;
}
let obj = {
x: 10,
y: 20
}
let fun = Point.prototype.toString.bind(obj);
fun(); // 30
fun(1, 2) // 30
obj.x = 1; obj.y = 2;
fun() // 3
上面fun是返回的新函数,fun(1, 2)也是输出30,证明this被永久绑定在obj了
使用new调用bind函数
当 bind 返回的函数作为构造函数,被new关键字调用的时候,bind时指定的this值会失效,但传入的参数依然生效。
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function() {
return this.x + this.y;
}
let obj = {
x: 10,
y: 20
}
let YAxisPoint = Point.bind(obj, 0); // 参数有用,但
let p = new YAxisPoint(5);
p.toString() // 5
bind的polyfill实现
直接放代码,用高阶函数和闭包实现
Function.prototype._bind = function() {
const target = this; // 保存原函数
let context = arguments[0], // 要被绑定的对象
args = Array.prototype.slice.call(arguments, 1); // 获取bind的时候的其他参数
let bound = function() {
let bindArgs = Array.prototype.slice.call(arguments);
// 如果是作为构造函数被调用,this指向实例对象
// 这里必须要return,否则得不到target函数的返回值
return target.apply(this instanceof target ? this : context, args.concat(bindArgs));
}
// 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型
// 原型对象是引用类型,如果prototype直接赋值,修改子类或者父类都会影响对方
if (target.prototype) {
var Empty = function() {};
Empty.prototype = target.prototype;
bound.prototype = new Empty(); // bound.prototype.__proto__ === target.prototype / true
Empty.prototype = null;
}
return bound;
}
被bound函数当作构造函数时
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function() {
return this.x + this.y;
}
let obj = {
x: 10,
y: 20
}
let bound = Point._bind(obj, 0); // 参数有用,但绑定的对象无效
let p = new bound(5);
p.toString() // 5
分析一下bound函数被当作构造函数使用的情况:
根据上面_bind函数的实现,会发现实际上调用Point._bind(obj, 0),是返回一个bound函数。
所以当bound函数作为构造函数的时候,let p = new bound(5)。发生了什么?
根据MDN的定义
绑定函数自动适应于使用 new 操作符去构造一个由目标函数创建的新实例。当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略。不过提供的参数列表仍然会插入到构造函数调用时的参数列表之前。
这是啥子意思,意思就是上面的例子中的_bind等于没起作用,obj被忽略了,let p = new bound(5)就相当于let p = new Point(5),但是之前传进去的0还是存在的,
所以最终效果是let p = new Point(0, 5),实际上,通过p.toString() // 5也能验证这个解释。
如何实现这种效果
实现这个效果要回看new关键字的过程,new的一个过程通常分为以下四步
- 创建一个空对象,作为将要返回的对象实例;
- 将这个空对象的连接到构造函数的原型对象;
- 将这个空对象作为构造函数this上下文;
- 如果该函数没有返回对象,则返回this;
第二步是关键,通常可以用obj.__proto__ = constructor.prototype去实现这个步骤,这里是原型的继承,不再赘述,所以关键就是如何将
let p = new bound(5)等价于let p = new Point(5)。
很简单,只要将bound.prototype = Point.prototype就行了,但是这样会产生一些问题,假设bound.prototype添加了一个方法,Point.prototype也会同步,因为prototype指向的是原型对象,是一个引用类型。
实际上两者会相互影响,如何消除这种影响,使用一个Empty函数作为中间值即可。
if (target.prototype) {
var Empty = function() {};
Empty.prototype = target.prototype;
bound.prototype = new Empty(); // bound.prototype === target.prototype / true
Empty.prototype = null;
}
这里涉及到原型的继承,不再赘述,实际上通过上面的操作我们可以发现
bound.prototype instanceof Point
// true
至此已经达到了我们的目的。
此时new进行第三个步骤,constructor.apply(obj, arguments),修改constructor的this上下文为obj,这个时候,bound内部的this应该指向obj,也就是新创建的对象。
此时调用了bound函数,所以这个时候,应该要将target函数的context改为实例对象obj,也就是this。target.apply(this, args.concat(bindArgs));
可以通过this instanceof target判断this是否继承于target从而得出是否通过new关键字调用函数。
return target.apply(this instanceof target ? this : context, args.concat(bindArgs));
至此,一个简单的bind实现已经完成了。
参考
developer.mozilla.org/zh-CN/docs/… developer.mozilla.org/zh-CN/docs/… github.com/Raynos/func…