JavaScript 回炉重造:this 绑定规则的优先级

558 阅读4分钟

前言

在之前的一篇this 的四种绑定规则文章中,我们已经了解了 this 的四种绑定规则。但,那都是在调用位置处只应用一条规则的情况下进行说明的。而本篇文章,则是主要介绍,当调用位置处应用多条规则时,我们应如何判断应用了哪条规则。这需要我们根据它们的优先级来进行筛选。

优先级

首先明确一点:默认绑定的优先级是四条规则中最低的。至于原因,在之前的文章中也提到过,那就是默认绑定规则可以看作是无法应用其他规则时的默认规则

隐式绑定和显式绑定的优先级比较

案例代码

function getNum() {
    console.log("this.a--->", this.a);
    console.log("this--->", this);
}

var obj1 = { a: 2, getNum: getNum };
var obj2 = { a: 3, getNum: getNum };

obj1.getNum();
console.log("----------------------------------");
obj1.getNum.call(obj2);

结果展示

截屏2021-09-17 17.28.19.png

通过观察上述代码的结果,我们可以看到,显式绑定优先级高于隐式绑定,所以在判断 this 绑定规则时,应当先考虑是否可以应用显式绑定。

new 绑定和隐式绑定的优先级比较

案例代码

function getNum(num) {
    this.a = num;
    console.log("this.a--->", this.a);
    console.log("this--->", this);
}

var obj1 = { getNum: getNum };

obj1.getNum(2); // 2

console.log("----------------------------------");

var bar = new obj1.getNum(4); // 注意, 这是 new 绑定和隐式绑定一起使用
console.log('obj1.a--->', obj1.a);
console.log('bar.a--->',bar.a);

结果展示

截屏2021-09-17 18.01.05.png

注意,上述代码对 new 绑定和隐式绑定的优先级进行比较的方式是通过 new obj1.getNum(4) 这行代码,即将它们俩个一起使用。

结果不言而喻,new 绑定的优先级高于比隐式绑定。

new 绑定和显示绑定的优先级比较

由于 new 和 call(或者 apply) 无法一起使用,所以我们无法通过 new foo.call(obj1) 这种方式来直接测试它们的优先级。但,好在我们还有硬绑定——bind。若是你有看过我之前关于 this 绑定规则的介绍,那你必然已知道,硬绑定也是一种显示绑定,因此我们可以使用硬绑定来测试 new 绑定和显示绑定的优先级。

案例代码

function getNum(num) {
    this.a = num;
    console.log("this.a--->", this.a);
    console.log("this--->", this);
}

var obj = {
    a: "",
};

var fn = getNum.bind(obj);
fn(2);

console.log("----------------------------------");

var instance = new fn(10);
console.log("obj.a--->", obj.a); // 2
console.log("instance.a--->", instance.a); // 10

在上述代码中,fn 被硬绑定到了 obj 上,但在执行 new fn(10) 后,new 修改了硬绑定到 obj的调用 fn() 中的 this。

结果展示

截屏2021-09-18 09.50.53.png

需要注意的是,new 能修改硬绑定到的 this 仅限于 ES5 中内置的——Function.prototype.bind 方法(其实现原理过于复杂,这里不做探讨,知道就行),换句话说,就是我们通过 call 或 apply实现的硬绑定是无法修改 this 的。例如,下面的这段代码:

function getNum() {
    console.log("this.a--->", this.a);
    console.log("this--->", this);
}

// 辅助函数
function bind(fn, obj) {
    return function () {
      fn.apply(obj, arguments);
    };
}

var obj = { a: 2 };
var bar = bind(getNum, obj);

bar();
console.log('---------------------------');
var ins = new bar();

结果展示

截屏2021-09-18 10.40.59.png

结果很明显,new 并没有修改硬绑定到 obj 的调用 bar() 中的 this。

判断 this

经过上面对 this 绑定规则优先级的比较,我们可以按照下面的顺序来判断函数在某个调用位置应用的是哪条规则:

  1. new 绑定

函数是否在 new 中调用?如果是,则 this 绑定的是新创建的对象。

var bar = new foo()
  1. 显式绑定

函数是否通过 call、apply 或者硬绑定调用?如果是,则 this 绑定的是指定的对象。

var bar = foo.call(obj)
  1. 隐式绑定

函数是否在某个上下文对象中调用?如果是,则 this 绑定的是那个上下文对象。

var bar = obj1.foo()
  1. 如果以上规则都不是的话,则使用默认绑定,this 绑定到全局对象或 undefined 上(这取决于代码是否启用了严格模式)。
var bar = foo()

就正常的函数调用来说,理解了上面的知识点,就足以让我们明白 this 的绑定原理。当然,凡事总有例外。

绑定例外

  1. 当你向 call、apply 和 bind 的第一个参数传入 null、undefined 或着空(不传值),这些值在调用时会被忽略,就会应用默认绑定规则。
function fn() { 
    console.log( this.a );
}

var a = 2;

fn.call(); // 2
fn.call( null ); // 2
fn.call( undefined ); // 2
  1. 无意间创建出一个函数的“间接引用”,在这种情况下,调用这个函数就会应用默认绑定规则。
function getNum() {
    console.log(this.a);
}

var a = 20;
var obj = {
    a: 2,
    getNum: getNum,
};

obj.getNum(); // 2

var fn = obj.getNum;
fn(); // 20

参考来源

  • 《你不知道JavaScript》上卷