likes
comments
collection
share

浅析this--this绑定规则及优先级

作者站长头像
站长
· 阅读数 3

1. 什么是this

1.1 this到底是什么

科学的尽头是神学,this关键字就是JS中极其复杂的一个机制,往往被人们神化,它自动被定义在每一个函数之中,就像是每一个函数的伴生品,亦正亦邪。使用恰当,往往会使得一段代码更加精短且强大,但往往会出现许多意外事件,使得它不稳定。实际上,this关键字并没有那么神秘,但是在缺乏清晰的认识下,this就像是一个魔法。今天,就让我们来揭开它那神秘的面纱。

       现在先声明一下,this是在运行时绑定的,它绑定哪个对象,完全依赖于它在声明时候的调用!this的绑定和声明位置并没有任何关系,只取决于它的调用位置

function demo(num){
    console.log(num);
    //记录demo的调用次数,待会赋值为0
    this.count++;
};

demo.count = 0;
demo(1); //1
demo(2); //2
demo(3); //3
//来看看demo.count为多少
console.log(demo.count);//0  ????what??

在上面我们可以很清晰的知道demo()函数一共是调用了3次,但是当我们输出计数器count时候却发现为0?

浅析this--this绑定规则及优先级

      所以,由此可见,从字面意思来理解this是错误的,this并非指向自身的。在执行demo.count()时,的确给函数增加了count,但是在函数内部的this.count指向的并不是外部的那个count,虽然两个属性名一样,但是却不是同一个属性。外部的count是一个全局变量,而this.count则是demo()的属性,为何会是这样,这就要从上下文来解释了。这里暂时不对上下文进行过多的阐述。

    那么,如何才可以解决上面出现的问题呢?既然两个是不同的count,那么我们让两个count变成同一个,不就可以解决了?

function demo(num) {
    console.log(num);    //记录demo的调用次数,待会赋值为0 
   demo.count++;  //将this.count改成demo.count
};

demo.count = 0;
demo(1); //1
demo(2); //2
demo(3); //3
console.log(demo.count);//3

通过这个方法,从某种层面来说,我们确实是解决了上述的问题,但是同时也是完美的避开了this。所以从某个方面来说,我们还是失败了。

浅析this--this绑定规则及优先级

1.2 为什么要使用this

        根据上述内容可以知道,this机制是如此的复杂,那么我们为什么还要使用它呢?它到底有什么用呢?它是否真的值得我们如此深入的去了解它吗?带着这一系列问题,我来为大家解释一下为什么我们还是要坚持使用如此复杂的this。

function upcase() { return this.name.toUpperCase();};
function test() { let demo = "demo:" + upcase.call(this);  console.log(demo);};
let name1 = { name: "demo1"};
let name2 = { name:"demo2" };
test.call(name1); //demo:DEMO1
test.call(name2); //demo:DMEO2

       如果未接触this或者对JS语法了解不够深入,那么这段代码往往是比较晦涩的。在这里,我们通过使用this,发现在不需要给upcase()和test()函数传入对象就可以使用其函数的功能。在过去不提及this,这就是一个神话。如果不使用this,就需要给upcase函数和test函数显式传入一个上下文对象,才可以完成这一操作,代码如下:

function upcase(name) { return name.name.toUpperCase();};
function test(name) { let demo = "demo:" + upcase(name);  console.log(demo);};
let name1 = {  name: "demo1"};
let name2 = {  name:"dmeo2"};

test(name1); //demo:DEMO1
test(name2); //demo:DMEO2

       通过两段代码比较,我们可以发现,使用this可以更加优雅的来传递一个对象的引用,可以使得我们的代码更加简洁且易于使用。尤其是在我们使用多个函数互相调用,不断套娃时,我们显示的使用的上下文传递对象,很容易使得代码混乱,且使代码可读性降低,使用this则不会这样。所以,使用this更有利于自己代码开发和后期代码维护。

1.3 它的作用域

    人们常常会理解为this是指向函数的作用域的,这种理解既可以说是正确的,也可以说是错误的。这是因为this机制的复杂性,决定了this的指向也是极其复杂的,所以说this是指向函数作用域,也不是指向函数的作用域。首先需要明确的是,**this是绝不指向函数的词法作用域的。**作用域和对象是很类似的,在内部的标识符都是他们的属性,但是对象是通过JS代码访问属性,而作用域则是在JS引擎内部,访问的区别导致了他们的不一致,同时也决定了this是不会指向函数的作用域的,在上文也提到过,this的绑定和声明位置并没有任何关系,只取决于它的调用位置this的绑定和声明位置并没有任何关系,只取决于它的调用位置!!this的绑定和声明位置并没有任何关系,只取决于它的调用位置!!!

浅析this--this绑定规则及优先级

2. 绑定规则

2.1 调用规则

       在理解在理解 this 的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。只有仔细分析调用位置才能回答这个 this 到底引用的是什么?或者说指向了什么。

       寻找调用位置就是寻找“函数被调用的位置”,但是做起来并没有这么简单,因为某些编程模式可能会隐藏真正的调用位置。最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的调用位置就在当前正在执行的函数的前一个调用中。

下面我们来看看到底什么是调用栈和调用位置:

function demo1() {
    // 当前调用栈是:demo1
    // 因此,当前调用位置是全局作用域
    console.log("demo1");
    dmeo2(); // <-- demo2 的调用位置 
};
function dmeo2() {
     // 当前调用栈是 demo1 -> demo2
     // 因此,当前调用位置在 demo1 中
     console.log("demo2");
     demo3(); // <-- demo3 的调用位置 
};
function demo3() {
    // 当前调用栈是 dmeo1 -> demo2 -> demo3
    // 因此,当前调用位置在 demo2 中
     console.log("demo3");
};
demo1(); // <-- demo1 的调用位置

注意我们是如何从调用栈中分析出真正的调用位置的,因为它决定了 this 的绑定。关于调用栈,你可以很简单的理解为函数调用的传送链,将不同的函数通过不断内嵌调用形成的一个路径,就像铁路线,你想去一个地方,需要根据铁路线不断转车才可以到达想去的地方,当然也不排除有些地方可以直达。就像demo1中可以直接到demo2中,但是到demo3却需要进入demo2才可以进入demo3。了解完如何寻找调用位置,我们就可以开始谈谈this的几个绑定规则了,这也是this入门的开始。

浅析this--this绑定规则及优先级

2.2 默认绑定

     首先出场的肯定是最简单最常用的函数调用类型:简单函数调用。可以把这看作是完全没有规则的一条规则,毕竟要给this找个对象。

function demo() {
    console.log(this.a);
};
var a = 2;
demo(); // 2  <--demo的真正引用位置

      首先应该注意到,声明在全局作用域中的变量var a = 2就是全局对象的一个属性。同时this.a引用的,就是全局变量a,因为demo声明在全局作用域中,所以this.a在demo内部没有找到属性a的情况下,就到全局对象上引用了a。或许有人问为什么?

       因为在本例中,函数调用时应用了 this 的默认绑定,因此 this 指向全局对象。那么我们怎么知道这里应用了_默认绑定_呢?我们只需要分析demo() 是如何调用的。在代码中,demo() 是直接进行调用的,因此只能使用默认绑定,无法应用其他规则。

       但是如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此 this 会绑定到 undefined:

function foo() { "use strict"; console.log( this.a ); }
var a = 2; foo(); // TypeError: this is undefined

        这里有一个非常重要的细节,虽然 this 的绑定规则完全取决于调用位置,但是只

有 foo() 运行在非 strict mode 下时,默认绑定才能绑定到全局对象;严格模式下与 foo()

的调用位置无关

function foo() { console.log( this.a ); }
var a = 2; (function(){ "use strict"; foo(); // 2 })(); 

       通常来说你不应该在代码中混合使用 strict mode 和 non-strict mode。整个程序要么严格要么非严格。然而,有时候你可能会用到第三方库,其严格程度和你的代码有所不同,因此一定要注意这类兼容性细节。

浅析this--this绑定规则及优先级

2.3 隐式绑定

隐式绑定则是另一条需要考虑的规则,在调用位置是否有上下文对象。

function demo() { console.log( this.a ); }
var obj = {
 a: 2, 
 demo: demo
};
obj.foo(); // 2

       首先需要注意的是 demo() 的声明方式,及其之后是如何被当作引用属性添加到 obj 中的。这个函数严格来说都不属于obj 对象。然而,调用位置会使用 obj 上下文来引用函数,因此你可以说函数被调用时 obj 对象“包含”它。当 demo() 被调用时,它的this确实指向 obj 对象。因此当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调用 demo() 时 this 被绑定到 obj,因此 this.a 和 obj.a 是一样的。

       既然存在隐式绑定,那么会不会存在隐式丢失呢?答案当然是肯定的!隐式丢失是一个最常见的一种丢失,通常存在与隐式绑定与默认绑定混用之中。

function foo() { console.log(this.a); }
var obj = { a: 2, foo: foo };
var bar = obj.foo; // 函数别名!调用后便会出现隐式丢失
var a = "oops, global"; // a 是全局对象的属性 
bar(); // "oops, global";

       虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定,this便会指向bar所在的作用域的a,出现隐式丢失。隐式丢失同时在回调函数中存在特别广泛,例如在JS内置的setTimeout() 函数之中,我们可以简写该函数便可以清楚的知道是如何丢失的。

function setTimeout(fn(),delay){

      delay(); //使得函数等待一段时间

      fn(); //<--调用位置   等待完毕开始执行函数。 }

       可见回调函数丢失 this 绑定是非常常见的。除此之外,还有一种情况 this 的行为会出乎我们意料:在一些流行的JS 库中事件处理器常会把回调函数的 this 强制绑定到触发事件的 DOM 元素上。这在一些情况下可能很有用,但是有时它可能会让你感到非常郁闷。遗憾的是,这些工具通常无法选择是否启用这个行为。

浅析this--this绑定规则及优先级

2.4 显式绑定

       当我们不想在对象内部对函数进行像隐式绑定那样引用,而想圆一霸王梦,在某个对象上强制调用函数,该怎么做呢?JS中的几乎每个函数都有一些有用的特性: call(..) 和apply(..) 方法。正如我们上门中第一次成功使用this的方式,他们会接受一个参数,接着把函数的this绑定到这个参数上,实现显式绑定。

function foo() { console.log( this.a ); }
var obj = { a:2 };
foo.call( obj ); // 2

       我们通过使用call()方法,传入我们想绑定的对象,将this绑定到该对象上。有时候当你传入的是一个简单数据类型,而非一个对象,这时候call方法会自动对其进行转换,转换成该数值的对象形式,然后进行绑定,这个过程业内又称之为“自动装箱”。

但是显示绑定虽然好用,但是任然会存在刚刚出现的隐式丢失,那么我们如何才可以解决这个问题呢?目前普遍使用的是两种办法

   1.硬绑定

       硬绑定属于显式绑定的一个变种,但是却可以解决这个问题。

function foo() {console.log( this.a ); }
var obj = { a:2 };
var bar = function() { foo.call( obj ); };
bar(); // 2 
setTimeout( bar, 100 ); // 2 
// 硬绑定的 bar 不可能再修改它的 this bar.call( window ); // 2

       在函数 bar()中,我们调用了 foo.call(obj),这样会强制把 foo 的 this 绑定到了 obj。无论之后如何调用函数 bar,this都会再次绑定到obj。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定

   2.API调用上下文

第三方库的许多函数,以及 JS语言和环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”,其作用和 bind(..) 一样,确保你的回调函数使用指定的 this

function foo(id) { console.log( id, this.id ); }
var obj = { id: "dmeo" };
// 调用 foo(..) 时把 this 绑定到 obj 
[1, 2, 3].forEach( foo, obj ); // 1 demo 2 demo3 demo

浅析this--this绑定规则及优先级

2.5 new绑定

       在了解new绑定前,我们首先了解一下使用new的时候,发生了什么,就可以很清楚的知道怎么进行了绑定。使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。

  2. 这个新对象会被执行原型链连接。

  3. 这个新对象会绑定到函数调用的 this。

  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。

所以根据上面,我们可以很清楚的知道在使用new的时候,他们就已经自动进行了一次绑定。

3. 绑定规则优先级

3.1 优先级比较

       接下来我们就要对上述几个绑定规则进行优先级比较,看看哪个的地位更高,并将它们进行一次排序,方便我们今后使用的时候更加清楚他们的绑定位置。

       首先默认绑定在最开始的时候就介绍了,是没有规则的时候才不得已使用的规则。所以是最没有牌面的

浅析this--this绑定规则及优先级

       接下来我们根据上文中在显示绑定中出现过解决隐式丢失的问题,如果说隐式丢失是隐式绑定的“爸爸”,那么显示绑定就是隐式绑定爸爸的爸爸,所以说隐式绑定在显示绑定面前就是个弟弟

浅析this--this绑定规则及优先级

所以显示绑定肯定是大于隐式绑定的!您要是真不信,那咱们,上代码!

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

        这回,您信了吧。所以现在的排名是显示>隐式>默认,那么new该插在哪里呢?擒贼先擒王,我们先直接把new和显示绑定进行比较

function foo(something) {this.a = something; }
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

现在看来,胜负已分!

new绑定成功把显示绑定给狠狠地敲打了一顿,直接百万军中,取显式之狗头!

所以我们对其四个规则进行排序:new绑定>显式绑定>隐式绑定>默认绑定

同时我们可以得出一套规律:

  1. 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。var bar = new foo()
  2. 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是指定的对象。var bar = foo.call(obj2)
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象。var demo2= obj.foo()
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到全局对象。demo();

浅析this--this绑定规则及优先级

4. 小结

如果要判断一个运行中的this绑定,重中之重就是找到它的调用位置,找到之后根据下面四条规则来判断this指针的绑定对象。

  1.   是否由new声明绑定到指定对象。
  2. 是否由call、apply或者bind调用。
  3. 是否上下文调用
  4. 默认绑定,在严格模式下为undefined,否则绑定到全局对象。

       同时要注意有时候会触发绑定例外,需要注意甄别。在ES6的箭头函数中并不会使用上列的绑定规则,而是根据当前的词法作用域来决定this的绑定对象。简单点说,就是箭头函数会直接继承上文函数调用的this绑定。

       因为篇幅的原因(其实我很努力压制,但是依旧成为一篇长文),在此只给大家简单的进行简单的this绑定规则的阐述,this的用法其实还是多种多样的,但是熟练掌握上述四种规则及优先级,就可以较为简单的进行this的绑定判断了!

浅析this--this绑定规则及优先级

转载自:https://juejin.cn/post/6925082635442225160
评论
请登录