一、调用位置
理解this绑定优先级的前提是理解this的绑定规则,理解绑定规则之前,我们先来了解一下函数"调用位置"。
通常来说,要想找到调用位置,最重要的是分析调用栈(在有的编程模式下,真正的调用位置可能被隐藏起来了,通过调用栈来分析调用位置最为直接)。
来个梨子:
function baz() {
// 当前调用栈是:baz
// 调用位置:全局作用域
console.log('baz');
bar();
}
function bar() {
// 当前调用栈是:baz -> bar
// 调用位置:baz中
console.log('bar');
foo();
}
function bar() {
// 当前调用栈是:baz -> bar -> foo
// 调用位置:bar中
console.log('foo');
}
baz(); // 全局调用
如我们在梨子中标注的一样,你可以把调用栈理解成一个函数链式调用。其实我们有一种更为简单的方式查找调用栈,那就是JavaScript开发者工具。如图。
二、绑定规则
接下来我们就看看函数在运行的过程中调用位置如何决定this的绑定对象。
1、默认绑定(独立函数调用)
var number = 1;
function baz() {
console.log(this.number);
}
baz(); // 1
当函数baz被调用时,this.number被解析成全局变量number。函数在调用时,进行默认绑定,此时的this指向全局对象(非严格模式),严格模式下this为undefined。
var number = 1;
function baz() {
"use strict"
console.log(this.number);
}
baz();
2、隐式绑定(对象方法调用)
function baz() {
console.log(this.number);
}
var object = {
number: 1,
baz: baz
};
object.baz(); // 1
函数baz()的声明方式,严格来说是不属于object对象的,但是调用位置会使用object上下文来引用函数。所以我们可以说object对象"拥有"或者"包含"baz()函数的引用。
隐式绑定,this丢失
function baz() {
console.log(this.number);
}
var object = {
number: 1,
baz: baz
};
var bar = object.baz();
var number = 2;
bar(); // 2
虽然bar是object.baz的一个引用,但是它是引用foo函数本身,因应用了默认绑定。this指向全局变量。
function baz() {
console.log(this.number);
}
function loadBaz(fn){
// fn其实就是引用的baz
fn(); // 回调函数的调用位置
}
var object = {
number: 1,
baz: baz
};
var number = 2;
loadBaz(object.baz); // 2
参数传递其实也是一种隐式的赋值,因此我们在传入函数时也会被隐藏赋值,所以,梨子2和梨子1是一样的结果。
3、显示绑定(apply、call、bind)
function baz() {
console.log(this.number);
}
var object = {
number: 1,
baz: baz
};
baz.call(object); // 1
// 或者baz.apply(object); // 1
4、new绑定
使用new来调用函数,或者说发生构造函数调用时,会执行下面的操作。
- 创建或者说构造一个全新的对象。
- 这个新对象会被执行[[Prototype]]连接。
- 这个新对象会绑定到函数调用的this。
- 如果函数没有返回其他队形,那么new表达式中的函数调用会自动返回这个新对象。
function baz(number) {
this.number = number;
}
var bar = new baz(1);
console.log(bar.a); // 1
使用new来调用baz()时,我们会构造一个新的对象并绑定到baz()调用中的this上。
三、优先级(本文主角)
前面简单讲解了this绑定的四条规则,你需要做的就是找到调用位置,判断使用那一条规则。但是,有时候,在一个调用位置可能使用了多条规则,应该如果判断了。这里就需要判断规则的优先级(如CSS的权重一样)。
判断1:默认绑定的优先级最低
判断2:隐式绑定和显示绑定谁的优先级高?
function foo() {
console.log(this.a);
}
var obj1 = {
a:2,
foo: foo
};
var obj2 = {
a:3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call(obj2); // 3
obj2.foo.call(obj1); // 2
判断3:隐式绑定和new绑定谁的优先级高?
function foo(a) {
this.a = a;
}
var obj1 = {
foo: foo
};
var obj2 = {};
obj1.foo(2);
console.log(obj1.a); // 2
obj1.foo.call(obj2, 3);
console.log(obj2.a); // 3
var bar = new obj1.foo(4);
console.log(obj1.a); // 2
console.log(bar.a); // 4
判断4:显示绑定和new绑定谁的优先级高?
function foo(a) {
this.a = a;
}
var obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); // 2
var baz = new bar(3);
console.log(obj1.a); // 2
console.log(baz.a); // 3
what?出乎意料呀。bar被绑定到obj1上,但是new bar(3) 并没有像我们预计的那样把obj1.a修改为3相反,new修改了绑定调用bar()中的this。那到底显示绑定和new绑定谁的优先级高?
我们来看看ES5内置的Function.prototype.bind()(显示绑定-强绑定)的实现。
MDN:Function.prototype.bind()的实现
if (!Function.prototype.bind) {
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() {
// this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用
return fToBind.apply(this instanceof fBound
? this
: oThis,
// 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
aArgs.concat(Array.prototype.slice.call(arguments)));
};
// 维护原型关系
if (this.prototype) {
// 当执行Function.prototype.bind()时, this为Function.prototype
// this.prototype(即Function.prototype.prototype)为undefined
fNOP.prototype = this.prototype;
}
// 下行的代码使fBound.prototype是fNOP的实例,因此
// 返回的fBound若作为new的构造函数,new生成的新对象作为this传入fBound,新对象的__proto__就是fNOP的实例
fBound.prototype = new fNOP();
return fBound;
};
}
在这段代码中,会判断绑定函数是否被new调用,。。。 之所以要在new中绑定函数,原因是预先设置函数的一些参数,这样在使用时,只需要传入剩余的参数。
根据上面的梨子:总结一下:
new绑定优先级 > 显示绑定优先级 > 隐式绑定优先级 > 默认绑定优先级
四、判断this(根据调用位置判断调用规则)
- 函数是否存在new绑定调用:如果是的话this绑定到新创建的对象上。
- 函数是否通过apply、call、bind显示绑定:如果是的话,this绑定到指定对象上。
- 函数是否在对象方法隐式调用:如果是的话,this绑定到调用对象。
- 如果上面三条都不满足的话:在严格模型下,this绑定到undefined,在非严格模式下,this绑定到全局对象上。
五、总结
希望这个文章能对阅读的你有所帮助。让我们一起成长吧。谢谢!
参考:《你不知道的JavaScript》