关于bind你不知道的(软硬)绑定

3,099 阅读4分钟

了解这个之前,需要大家对this绑定有所了解。this绑定可以细分为默认绑定,隐式绑定,显式绑定(硬绑定)和new绑定四种。我们今天主要来讲硬绑定中bind的实现细节以及如何修改bind来达到更灵活的绑定应用(这里称为软绑定)

this的绑定规则

每个函数的 this 是在调用时被绑定的,完全取决于函数的调用位置(也就是函数的调用方法)。

默认绑定

独立函数在运行的时候,函数的调用是不带任何修饰的,无法应用其他规则时的默认规则即为默认绑定。在严格模式下面,默认绑定会绑定到undefined

function foo() { 
	console.log( this.a );
}
var a = 2; 
foo(); // 2


function foo() { 
	"use strict";
    console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined

这里有一个微妙但是非常重要的细节,虽然 this 的绑定规则完全取决于调用位置,但是只 有 foo() 运行在非 strict mode 下时,默认绑定才能绑定到全局对象;严格模式下与 foo() 的调用位置无关.

function foo() { 
	 console.log( this.a );
}
var a = 2;
(function(){
	"use strict";
	 foo(); // 2
})();

隐式绑定

隐式绑定规则是指调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。

function foo() { 
	console.log( this.a );
}
var obj = { a: 2, foo: foo };
obj.foo(); // 2

调用位置会使用 obj上下文来引用函数,因此你可以说函数被调用时 obj对象“拥 有”或者“包含”它。无论你如何称呼这个模式,当 foo() 被调用时,它的落脚点确实指向 obj对象。当函数引 用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调用 foo()this 被绑定到 obj,因此 this.aobj.a 是一样的。

显示绑定

可以使用函数的call(..)apply(..)方法传入绑定对象,它们会把这个对象绑定到this,接着在调用函数时指定这个 this。因为你可以直接指定this的绑定对象,因此我们称之为显式绑定。

function foo() { 
	console.log( this.a );
}
var obj = { a:2 };
foo.call( obj ); // 2

如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对 象,这个原始值会被转换成它的对象形式(也就是new String(..)、new Boolean(..)或者 new Number(..))。这通常被称为“装箱”。

new 绑定

使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行[[原型]]连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function foo(a) { 
	this.a = a;
}
var bar = new foo(2); 
console.log( bar.a ); // 2

使用 new 来调用foo(..)时,我们会构造一个新对象并把它绑定到 foo(..)调用中的 this上。new是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。

bind的代码实现

if (!Function.prototype.bind) { 
	Function.prototype.bind = function(oThis) {
		if (typeof this !== "function") { 
			// 与 ECMAScript 5 最接近的 // 内部 IsCallable 函数
			throw new TypeError(
		        "Function.prototype.bind - what is trying " +
	            "to be bound is not callable"
	            ); 
	          }
	var aArgs = Array.prototype.slice.call( arguments, 1 ), 
	fToBind = this,
	fNOP = function(){}, 
	fBound = function() {
		return fToBind.apply(
			(
				this instanceof fNOP &&
				oThis ? this : oThis 
			),
		    aArgs.concat(
		        Array.prototype.slice.call( arguments )
		    ); 
		}
		fNOP.prototype = this.prototype;
		fBound.prototype = new fNOP(); 
		return fBound;
	}
}

这里有个很有意思的点就是:

this instanceof fNOP && oThis ? this : oThis // ... 以及: fNOP.prototype = this.prototype; fBound.prototype = new fNOP();

简单来说,这段代码会判断硬绑定函数是否是被 new 调用,如果是的话就会使用新创建的 this 替换硬绑定的 this。用代码表示就是:

var obj = {val: 123}

function foo(value) {
  if(value) this.val = value;
	console.log(this.val)
}
var bar = foo.bind(obj);
bar() // 123
var baz = new bar("p2");
baz.val; // p2
// 原型链接
baz.__proto__ === bar.prototype === fBound.prototype === new fNOP() === fNOP.prototype

软绑定

之前我们已经看到过,硬绑定这种方式可以把 this 强制绑定到指定的对象(除了使用 new 时),防止函数调用应用默认绑定规则。问题在于,硬绑定会大大降低函数的灵活性,使 用硬绑定之后就无法使用隐式绑定或者显式绑定来修改 this。如果可以给默认绑定指定一个全局对象和 undefined 以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改 this 的能力。可以通过一种被称为软绑定的方法来实现我们想要的效果:

if (!Function.prototype.softBind) { 
	Function.prototype.softBind = function(obj) {
		var fn = this;
		// 捕获所有 curried 参数
		var curried = [].slice.call( arguments, 1 ); 
		var bound = function() {
			return fn.apply(
				(!this || this === (window || global)) ?
					obj : this
					curried.concat.apply( curried, arguments )
				); 
			};
		bound.prototype = Object.create( fn.prototype );
		return bound; 
	};
}

它会对指定的函数进行封装,首先检查调用时的this,如果 this 绑定到全局对象或者 undefined,那就把指定的默认对象 obj 绑定到 this,否则不会修改this

function foo() {
	console.log("name: " + this.name);
}
var obj = { name: "obj" }, 
	obj2 = { name: "obj2" }, 
	obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj); 
obj2.foo(); // name: obj2 <---- 看!!!
fooOBJ.call( obj3 ); // name: obj3 <---- 看! 
setTimeout( obj2.foo, 10 ); // name: obj <---- 应用了软绑定

可以看到,软绑定版本的 foo() 可以手动将 this 绑定到 obj2 或者 obj3 上,但如果应用默认绑定,则会将 this 绑定到 obj

到这里就结束了,希望对你有些帮助。


参考:《你不知到的javaScript》