JavaScript-this绑定的优先级

一、调用位置

理解this绑定优先级的前提是理解this的绑定规则,理解绑定规则之前,我们先来了解一下函数"调用位置"。

\color{red}{调用位置就是函数在代码中被调用的位置,而不是函数声明的位置。}

通常来说,要想找到调用位置,最重要的是分析调用栈(在有的编程模式下,真正的调用位置可能被隐藏起来了,通过调用栈来分析调用位置最为直接)。

来个梨子:

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来调用函数,或者说发生构造函数调用时,会执行下面的操作。

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

使用new来调用baz()时,我们会构造一个新的对象并绑定到baz()调用中的this上。

三、优先级(本文主角)

前面简单讲解了this绑定的四条规则,你需要做的就是找到调用位置,判断使用那一条规则。但是,有时候,在一个调用位置可能使用了多条规则,应该如果判断了。这里就需要判断规则的优先级(如CSS的权重一样)。

判断1:默认绑定的优先级最低

\color{red}{毫无疑问,在四条规则中,默认绑定的优先级是四条规则中最低的。我们先可以不用管它。}

判断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 

\color{red}{在梨子中我们可以看到,显示绑定的优先级更高。}

判断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

\color{red}{在梨子中我们可以看到,new绑定的优先级更高。}

判断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调用,\color{red}{如果是的话就使用新创建的this替换绑定的this}\color{red}{换句话说,在使用显示绑定的时候,如果被new绑定调用}\color{red}{显示绑定this优先级是低于new绑定this优先级}。 之所以要在new中绑定函数,原因是预先设置函数的一些参数,这样在使用时,只需要传入剩余的参数。

根据上面的梨子:总结一下:

new绑定优先级 > 显示绑定优先级 > 隐式绑定优先级 > 默认绑定优先级

四、判断this(根据调用位置判断调用规则)

  1. 函数是否存在new绑定调用:如果是的话this绑定到新创建的对象上。
  2. 函数是否通过apply、call、bind显示绑定:如果是的话,this绑定到指定对象上。
  3. 函数是否在对象方法隐式调用:如果是的话,this绑定到调用对象。
  4. 如果上面三条都不满足的话:在严格模型下,this绑定到undefined,在非严格模式下,this绑定到全局对象上。

五、总结

希望这个文章能对阅读的你有所帮助。让我们一起成长吧。谢谢!

参考:《你不知道的JavaScript》