最近在看《你不知道的JavaScript》系列,看到这个地方的时候,第一眼没对上,没有确认过的眼神,所以就带着疑惑,深入解析一下,做了一份学习总结。
Function.prototype.bind
引用 MDN:
bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。语法:
fun.bind(thisArg[, arg1[, arg2[, ...]]])参数:
thisArg:当绑定函数被调用时,该参数会作为原函数运行时的 this 指向。当使用 new 调用绑定函数时,该参数无效。
arg1, arg2, ...:当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。返回值:
返回由指定的
this值和初始化参数改造的原函数拷贝
从上面的定义来看,bind 函数有哪些功能:
改变原函数的
this指向,即绑定this返回原函数的拷贝
注意,还有一点,当
new调用绑定函数的时候,thisArg参数无效。也就是new操作符修改this指向的优先级更高
bind 函数的实现
bind 函数的实现,需要了解 this 的绑定。this 绑定有 4 种绑定规则:
默认绑定
隐式绑定
显式绑定
new 绑定
四种绑定规则的优先级从上到下,依次递增,默认绑定优先级最低,new 绑定最高。今天我们来讨论一下显式绑定。
显式绑定就是,运用 apply(...) 和 call(...) 方法,在调用函数时,绑定 this,也即是可以指定调用函数中的 this 值。例如:
function foo() {
console.log(this.a);
}
var obj = { a: 2 };
foo.call(obj); // 2这是不是 bind 函数的功能之一,修改 this 的绑定?如果我们将上面的例子修改一下:
Function.prototype.myBind = function(oThis) {
if(typeof this !== 'function') {
return;
}
var self = this,
args = Array.prototype.slice.call(arguments, 1);
return function() {
return self.apply(oThis, args.concat(Array.prototype.slice.call(arguments)));
}
}
function foo() {
console.log(this.a);
}
var obj = { a: 2 };
var bar = foo.myBind(obj);
bar(); // 2这便是一个简易版的 bind 函数了,已实现了原生 bind 函数的前两个功能点了。
但是,如果遇到 new 调用绑定函数(注意这里哈,是绑定之后的函数)的时候,结果会是怎样呢?
function foo(name) {
this.name = name;
}
var obj = {};
var bar = foo.myBind(obj);
bar('Jack');
console.log(obj.name); // Jack
var alice = new bar('Alice');
console.log(obj.name); // Alice
console.log(alice.name); // undefined我们发现,new 调用绑定函数,并不会更改 this 的指向,我们简易版能做的,只是永久绑定指定的 this。
如何实现原生 bind 的第三个功能点呢?
实现之前,我们来了解一下,new 操作符在调用构造函数的时候,会进行一个什么样的过程:
创建一个全新的对象
这个对象被执行
[[Prototype]]连接将这个对象绑定到构造函数中的
this如果函数没有返回其他对象,则
new操作符调用的函数则会返回这个对象
这可以看出,在 new 执行过程中的第三步,会对函数调用的 this 进行修改。在我们简易版的 bind 函数里,原函数调用中的 this 永远执行指定的对象,而不能根据如果是 new 调用而绑定到 new 创建的对象。所以,我们要对原函数的调用进行判断,是否是 new 调用。我们再对简易版 bind 函数进行修改:
Function.prototype.myBind = function(oThis) {
if(typeof this !== 'function') {
return;
}
var self = this,
args = Array.prototype.slice.call(arguments, 1),
fBound = function () {
return self.apply(
// 检测是否是 new 创建
(this instanceof self ? this : oThis),
args.concat(Array.prototype.slice.call(arguments))
);
};
// 思考下为什么要链接原型?提示:如果不连接,上面的检测是否会成功
if(this.prototype) {
fBound.prototype = this.prototype;
}
return fBound;
}
// 测试
function foo(name) {
this.name = name;
}
var obj = {};
var bar = foo.myBind(obj);
bar('Jack');
console.log(obj.name); // Jack
var alice = new bar('Alice');
console.log(obj.name); // Jack
console.log(alice.name); // Alice经过修改之后,此时我们发现, myBind 函数已经实现原生 bind 函数的功能。在上述代码中,留下一个问题,在这里讲一下:
首先,变量
bar是绑定之后的函数,也就是fBound。self是原函数foo的引用。对于
fBound函数中的this的指向,如果是bar('Jack')这样直接调用,this指向全局变量或者undefined(视是否在严格模式下)。但是如果是new bar('Alice'),根据上面给出的new执行过程,我们知道,fBound函数中的this会指向new表达式返回的对象,即alice。捋清楚变量之后,我们接着分析。我们首先忽略掉原型连接,也即忽略
fBound.prototype = this.prototype这行代码。如果是直接调用
bar('Jack'),this instanceof self ? this : oThis这句判断,根据上述变量分析,所以此判断为false,绑定函数的this指向oThis,也即是指定的this对象。如果是
new调用绑定函数,此时绑定函数中的this是由new调用绑定函数返回的实例对象,这个对象的构造函数是fBound,当我们忽略掉原型连接那行代码时,其原型对象并不等于原函数self的原型,所以this instanceof self ? this : oThis得到的值还是指定的对象,而不是new返回的对象。所以,知道为什么要在绑定的时候,绑定函数要与原函数进行原型连接了吧?每次绑定的时候,将绑定函数
fBound的原型指向原函数的原型,如果new调用绑定函数,得到的实例的原型,也是原函数的原型。这样在new执行过程中,执行绑定函数的时候对this的判断就可以判断出是否是new操作符调用
好了,到这基本结束了。
哦,是么?
等等,在原型连接的时候,你们是否发现 fBound.prototype = this.prototype 这赋值是有问题的?
哦,对哦。
当绑定函数直接连接原函数的原型的时候,如果 fBound 的原型有修改时,是不是原函数的原型也会受到影响了?所以,为了解决这个问题,我们需要一个空函数,作为中间人。
Function.prototype.bind = function(oThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
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
? this
: oThis,
// 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
aArgs.concat(Array.prototype.slice.call(arguments)));
};
// 维护原型关系
if (this.prototype) {
// Function.prototype doesn't have a prototype property
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();
return fBound;
};上述代码是 MDN 提供 bind 函数的 Polyfill 方案,里面的细节我们都分析完毕了,到这基本理解 bind 函数实现的功能的背后了。
主要的知识点:
this的绑定规则new操作符执行过程原型
参考书籍:
《你不知道的 JavaScript》(上卷)