最近在看《你不知道的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》(上卷)