前言
从本文你能够获得:
- 理解this的绑定机制及其优先级
- 学会使用apply/call实现bind函数
- 学会不使用apply/call实现bind函数
废话不多说,我们一起来看看this的绑定机制。
this绑定机制
开局上结论this有四种绑定模式分别是默认绑定、隐式绑定、显式绑定、new绑定。
他们之间的优先级关系为:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定。
让我们来看一些例子分清这几种绑定:
例子1:默认绑定
// 默认绑定
var str = 'hello world'
function log() {
console.log(this.str)
}
// 此时this默认指向window
log() // hello world
// 在严格模式下this默认指向undefined的
'use strict'
var str2 = 'hello world'
function log() {
console.log(this.str2)
}
// 此时this指向undefined
log() // 报错TypeError,因为程序从undefined中获取str2
例子2:隐式绑定
// 隐式绑定一般发生在函数作为对象属性调用时
var bar = 'hello'
function foo() {
console.log(this.bar)
}
var obj = {
bar: 'world',
foo: foo
}
foo() // hello,this指向window所以输出hello
obj.foo() // world,this隐式绑定了obj,这时候this指向obj所以输出world
列子3:显式绑定
// 显式绑定就是我们常谈的apply,call,bind
var bar = 'hello'
var context = {
bar: 'world'
}
function foo() {
console.log(this.bar);
}
foo() // hello
foo.call(context) // world 可见此时this的指向已经变成了context
列子4:new绑定
new绑定比较特殊,new大部分情况下是创建一个新的对象,并将this指向这个新对象,最后返回这个对象。
function Foo(bar) {
this.bar = bar
}
// 创建一个新的对象,并将this指向这个对象,将这个对象返回赋值给foo
var foo = new Foo(3);
foo.bar // 3
说完this的绑定类型,我们考虑下下面的代码的输出
var context = {
bar: 2
}
function Foo() {
this.bar = 'new bar'
}
var FooWithContext = Foo.bind(context);
var foo = new FooWithContext();
// 考虑下面代码的输出
console.log(foo.bar)
console.log(context.bar)
// 结果是:new bar 2
/**
* 我们可以发现虽然将使用bind函数将this绑定到context上,
* 但被new调用的Foo,他的this并没有绑定到context上。
*/
四种this绑定的优先级验证
从上述例子2可以推断隐式绑定优先级是高于默认绑定的,所以这里我们只推导后续三种的绑定的优先级关系。
显式绑定和new绑定
例子4:
// 我们先验证隐式绑定和显式绑定的优先级关系
var context = {
bar: 1
}
var obj = {
bar: 2
}
function foo() {
// 对bar进行赋值
this.bar = 3;
}
// 进行显式绑定
var fooWithContext = foo.bind(context);
obj.foo = fooWithContext;
obj.foo()
console.log(obj.bar); // 2
console.log(context.bar); // 3
// 可见foo并没有改变obj.bar的值而是创建了一个新对象,符合我们对new绑定的描述
根据上面的列子我们可以得出结论new绑定 > 显式绑定
另外几种绑定的优先级情况
根据例子2和例子3,我们可以轻松推导出隐式绑定和显式绑定要优先级高于默认绑定
我们验证一下隐式绑定和显式绑定的优先级关系。
var obj = {
bar: 2
}
var context = {
bar: 3
}
function foo () {
this.bar = 4
}
// 将foo的this绑定到context上
var fooWithContext = foo.bind(context);
// 将绑定后的函数,赋值给obj的属性foo
obj.foo = fooWithContext;
obj.foo()
console.log(obj.bar); // 2 并没有改变obj.bar的值
console.log(context.bar); // 4 context.bar的值发生了改变
可见显式绑定的this优先级要高于隐式绑定
最后我们便可以得出结论new绑定 > 显式绑定 > 隐式绑定 > 默认绑定。
用apply实现bind
刚刚说了那么多this的绑定问题,这到底和我们实现bind有什么关系?
我们来看看一段简单的bind的实现代码:
Function.prototype.bind(context, ...args) {
const fn = this;
return (...innerArgs) => {
return fn.call(context, ...args, ...innerArgs)
}
}
这个bind函数,在大部分情况都是能正常工作的,但是我们考虑如下场景:
function foo() {
this.bar = 3
}
var context = {
bar: 4
}
var fooWithContext = foo.bind(context);
var fooInstance = new fooWithContext();
console.log(context.bar) // 3
可以看到,被new调用后的foo,在运行时this依然指向context,这不符合我们刚刚根据原生方法推断的绑定优先级:new绑定 > 显式绑定
所以我们在实现bind的时候,需要考虑维护new调用的情况
我们来看看如何实现一个真正的bind:
Function.prototype.bind(context, ..args) {
var fToBind = this;
// 先声明一个空函数,用途后面介绍
var fNop = function() {};
var fBound = function(...innerArgs) {
// 如果被new调用,this应该是fBound的实例
if(this instanceof fBound) {
/**
* cover住new调用的情况
* 所以其实我们这里要模拟fToBind被new调用的情况,并返回
* 我们使用new创建的对象替换掉bind传进来的context
*/
return fToBind.call(this, ...args, ...innerArgs)
} else {
// 非new调用情况下的正常返回
return fToBind.call(context, ...args, ...innerArgs)
}
}
// 除了维护new的this绑定,我们还需要维护new导致的原型链变化
// 执行new后返回的对象的原型链会指向fToBind
// 但是我们调用bind后实际返回的是fBound,所以我们这里需要替换掉fBound的原型
fNop.prototype = this.prototype;
// fBound.prototype.__proto__ = fNop.prototype
fBound.prototype = new fNop();
/**
* 这样当new调用fBound后,实例依然能访问fToBind的原型方法
* 为什么不直接fBound.prototype = this.prototype呢
* 考虑下将fBound返回后,给fBound添加实例方法的情况
* 即fBound.prototype.anotherMethod = function() {}
* 如果将fToBind的原型直接赋值给fBound的原型,添加原型方法就会
* 污染源方法即fToBind的原型
*/
return fBound
}
到这里我们就实现了一个符合原生表现的bind函数,但是有时候架不住有人问那不用apply和call如何实现bind呢?接下来我们使用隐式绑定来实现一个bind
不使用apply和call实现bind
我们刚刚分析完实现bind的实现需要注意的点,这里就不重复说明了,我们看看如何使用隐式绑定来模仿bind。
// 我们把关注点放在如何替换call方法上
Function.prototype.bind(context, ...args) {
var fToBind = this;
var fNop = function() {};
var fBound = function(...innerArgs) {
// 我们将fToBind赋值给context一个属性上。
context.__fn = fToBind;
if(this instanceof fBound) {
// 模拟new调用,创建一个新对象,新对象的原型链指向fBound的原型
var instance = Object.create(fBound);
instance.__fn = fToBind;
var result = instance.__fn(...args, ...innerArgs);
delete instance.__fn;
// new调用时,如果构造函数返回了对象,使用返回的对象替换this
if(result) return result;
return instance;
} else {
// 在__fn没有显式绑定的情况下,__fn运行时this指向context
var result = context.__fn(...args, ...innerArgs);
// 调用完后将context的__fn属性删除
delete context.__fn;
return result;
}
}
fNop.prototype = this.prototype;
fBound.prototype = new fNop();
return fBound;
}
到这里不使用apply实现的bind就大功告成了
总结
我来总结下一共有哪些点需要认清楚:
- this有四种绑定模式,默认绑定、隐式绑定、显式绑定、new绑定
- this四种绑定的优先级关系:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
- 实现bind需要额外维护new绑定的情况
看了这么多可能会有朋友问,箭头函数呢?
欢迎阅读我的另外一篇文章:箭头函数中的this
参考资料:
- MDN: Function.prototype.bind
- MDN: new 操作符
发表回复