如何简单实现一个bind函数

1,499 阅读4分钟

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的一个过程通常分为以下四步

  1. 创建一个空对象,作为将要返回的对象实例;
  2. 将这个空对象的连接到构造函数的原型对象;
  3. 将这个空对象作为构造函数this上下文;
  4. 如果该函数没有返回对象,则返回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…