【JavaScript复习】【一篇就够】浅析this--this绑定规则及优先级

本文深入浅出地讲解了JavaScript中的this绑定机制,包括其绑定规则、优先级以及特殊情况下的处理方式。

1.什么是this?

        1.1 this到底是什么?

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

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

来看一个实例:

function foo(){
    console.log(this);
}
foo() //window
let obj ={
   name:'sss',
   foo:foo
}
obj.foo()//{name:'sss',foo:function}
foo.call("abc")  // 生成包装类型对象,String {'abc'}

        我们会发现,定义一个函数,用三种不同的方式调用,会产生三种不同的结果。一个函数调用方式不同this指向也不尽相同,为什么会出现这么令人费解的现象呢?是因为在JavaScript中函数的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是指向函数作用域,也不是指向函数的作用域。首先需要明确的是,**this是绝不指向函数的词法作用域的。**作用域和对象是很类似的,在内部的标识符都是他们的属性,但是对象是通过JS代码访问属性,而作用域则是在JS引擎内部,访问的区别导致了他们的不一致,同时也决定了this是不会指向函数的作用域的,在上文也提到过,重要的事情说三遍: this的绑定和声明位置并没有任何关系,只取决于它的调用位置this的绑定和声明位置并没有任何关系,只取决于它的调用位置!!this的绑定和声明位置并没有任何关系,只取决于它的调用位置!!!

2.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的几个绑定规则了,这也是this入门的开始。

        2.1 默认绑定

        首先出场的肯定是最简单最容易理解的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。

function foo(){
    console.log(this); 
}
foo()  // 独立函数调用 
let obj ={
    name:'sss',
     bar:function(){
         console.log(this);
    }
}
let ff = obj.bar
ff() // 函数在对象中,但是独立函数独立函数调用
// 高阶函数
function test(fn){
     fn()
}
test(obj.bar) //相当于fn=obj.bar fn() 所以还是独立函数调用

结果如下图所示:

        2.2 隐式绑定

        隐式绑定规则考虑的是调用位置是否有上下文对象。

let obj ={
    name:'sss',
     bar:function(){
         console.log(this);
    }
}
obj.bar() // 通过对象调用,结果是 obj

        调用位置会使用 obj 上下文来引用函数,因此你可以说函数被调用时 obj 对象“包含”它。当 bar() 被调用时,它的this确实指向 obj 对象。因此当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调用 bar() 时 this 被绑定到 obj,因此 this 和 obj是一样的。

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

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 元素上。这在一些情况下可能很有用,但是有时它可能会让你感到非常郁闷。遗憾的是,这些工具通常无法选择是否启用这个行为。

例如:

function foo(){
    console.log(this);
}
setTimeout(foo,1000) //Window
let btn = document.querySelector('button')
btn.addEventListener('click',foo) //button
let ar = [1,2,3,4]
ar.forEach(item=>console.log(this)) //Window

        在该例中,setTimeout内部绑定的this是window ,btn的this指向按钮,forEach方法指向Window 。

        2.3 显式绑定

         当我们不想在对象内部对函数进行像隐式绑定那样引用,而想在某个对象上强制调用函数,该怎么做呢?

        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

        2.4 new绑定

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

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

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

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

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

function person(name) {
    this.name =name
    console.log(this);
}
new person('sss') // person{name:'sss'}

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

3.绑定规则优先级

        3.1 优先级比较

        就像运算符有优先级规则一样,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();

        3.2 小结

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

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

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

熟练掌握上述四种规则及优先级,就可以较为简单的进行this的绑定判断了!希望本文可以帮助到大家,如果觉得还不错的话,点个赞再走吧!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码上游

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值