一文看穿JavaScript中this的圈圈绕

导文目录


  • 为何说JavaScript中 this 指针圈圈绕?
  • JavaScript 中 this 绑定做用域的四种状况
    • 先搞清Node环境中和浏览器环境中全局对象的异同
    • 默认绑定
    • 隐式绑定
    • 硬绑定(或者说 显示绑定)
    • new操做符绑定
    • 四种绑定的优先级
  • ES6中引入箭头函数对this的绑定产生了什么影响?
  • 附上前面程序的输出答案

为何说JavaScript中 this 指针圈圈绕?


相比C++或者Java中的this指针的概念而言,JavaScript中的this指针更为 "灵活" ,C++或Java中的 this在类定义时便成为了一个指向特定类实例的指针,可是JavaScript中的this指针是能够动态绑定的,也就是说是依据上下文环境来指定this到底指向谁。这样的机制给编程带来了很大的灵活性(以及趣味性),但这也致使了在JavaScript编程中若不明白this指针的做用机制而滥用this指针的话,经常会引起一些 "莫名其妙" 的问题。好比说,下面这段程序:javascript

1. let num = 10;
2. console.log(global.num); 
3. global.num = 20;
4. console.log(this.num);
5. console.log(this === global); 
6. function A(){
7.    console.log(this.num++);
8. }
9. let obj = {
10.     num : 30,
11.     B : function(){
12.         console.log(this.num++);
13.         return () => console.log(this.num++);
14.      }
15. }
16. A();
17. let b = obj.B; 
18. b()();  
19. obj.B();   
20. b.apply(obj);
21. new A(); 
22. console.log(global.num); 
复制代码

你能列出最终全部的输出吗?你能够先尝试着写一下,不要复制到VSCode中运行哦~ ,手动写出答案!写完先看一下最后面的答案,看你是否写对了。若是写对了说明你已经基本掌握了JavaScript中this指针的机制 (PS:设定这里运行环境是node环境 ;若是没有写对,那看完本文相信就能够对this有一个基本清楚的认识了。java

相信你确定忍不住去看了答案了,或许答案看起来杂乱无章,这也是为何this做为JavaScript中最复杂的机制之一常常被拿到面试中去考察JS的功底。如下内容可能须要花费8-10分钟时间,可是会让读者你受益不浅的,你的疑问也能够在下面的内容中获得解答!node

JavaScript 中 this 绑定做用域的四种状况


先搞清Node环境中和浏览器环境中全局对象的异同

在讲解this绑定做用域的四种状况以前,咱们先要弄清楚一个问题。Node环境中的全局做用域和浏览器环境下的全局做用域有什么不一样?面试

这个问题很重要,由于这个异同,会致使一样的代码在Node环境和浏览器环境下的表现不尽相同。就好比咱们这里要讲的this指针的指向会由于环境不一样而不一样。这个不一样体如今如下三点:编程

  • 浏览器的全局做用域的全局对象是window ; 而Node中这个"等价"的全局对象是global
  • 浏览器环境下全局做用域下的this指向的就是window对象; 可是Node环境下全局做用域中的thisglobal是分离的,this指针指向一个空对象
  • 浏览器环境下全局做用域中声明的变量会被认为是全局对象window的属性;可是Node下全局做用域下的声明的变量不属于global

由此,你即可知,上面代码中1-5的输出了,就像下面这样:浏览器

undefined  // 1 
undefined  // 2
false      // 3
复制代码

为了方便讲解,我给每一个输出编了号,咱们依次来看:微信

  1. 第一个undefined是由于Node的全局做用域上的变量并不会做为global的属性,此时global.num还没有赋值,因此是undefined
  2. 第二个undefined是由于Node中全局做用域中的this并不指向global,因此此时this.num还没有赋值,因此也是undefined
  3. 第三个false也更加应证了 2 中的结论,Node中全局做用域的thisglobal风马牛不相及

【PS】上面我一直强调是全局做用域下的this ,为何呢?由于Node中在子做用域中的this的行为和浏览器中是相仿的,基本一致闭包

默认绑定

下面咱们来说解JavaScript中this绑定做用域的四种状况。app

先来讲第一种——默认绑定 ,咱们能够这样理解 默认绑定 ,this指针在做用域内没有认领的对象时就会默认绑定到全局做用域的全局对象中去,在Node中就是会绑定到global对象上去。这是一个很形象的说法,虽然不严谨可是好理解,咱们看下面这几个例子,来讲明什么状况下,this没有对象认领。函数

global.name = 'javascript';
(function A(){
    this.name += 'this';
    console.log(this.name);//输出 javascriptthis
})();
console.log(global.name);//输出 javascriptthis
复制代码

在函数A的做用域内,this并无能够依靠的对象,因此this指针便开启默认绑定模式,此时指向的是global

这里咱们有必要明确一个概念,有别于JavaScript中"一切皆为对象"的概念,虽然A确实是一个Function类型的对象 , 下面的例子可证实确实如此

function A(){}
console.log(A instanceof Function); //输出 true
console.log(A instanceof Object);   //输出 true
复制代码

可是function A(){}只是一个函数的声明,并无实例对象的产生,而this是须要依托于一个存在的实例对象 , 若是使用new A()则便有了实例对象,this也就有了依托,有关new操做符绑定在后面说。

明白了这一点,咱们来看一个更为复杂的例子:

global.name = 'javascript';
(function A(){
    this.name += 'this';
    return function(){
        console.log(this.name);//输出 javascriptthis
    }
})()();
console.log(global.name);//输出 javascriptthis
复制代码

这个例子中函数A返回了一个匿名函数也能够叫闭包,咱们发现this照样绑定在了global上。这个例子是想告诉读者,默认绑定和做用域层级没有关系,只要是在做用域内this找不到认领的实例对象,那就会启用默认绑定。

由此,你是否是能够知道开篇的例子中 6,7,8,16行的输出结果了?

20  //这里是后置自增运算,因此先输出后加一
复制代码

隐式绑定

隐式绑定顾名思义没有显式的代表this的指向,可是已经绑定的某个实例对象上去了。举个简单的例子,这个用法实际上是咱们最经常使用的:

global.name = 'javascript' ;
let obj = {
    name : 'obj',
    A    : function(){
        this.name += 'this';
        console.log(this.name); 
    }
}
obj.A();//输出 objthis
console.log(global.name);//输出 javascript
复制代码

这个例子中函数A的做用域内,this总算是有对象认领了,这个对象就是obj,因此this.name指向的就是obj中的name ,这种状况就叫作隐式绑定

隐式绑定虽然是咱们最经常使用的,也是相对好理解的一种绑定方式,可是确是四种绑定中最坑的一种,为何呢?由于,这种状况下this一不当心就会找不到认领她的对象了,咱们称之为"丢失"。而在"丢失"的状况下,this的指向会启用默认绑定。咱们看下面的例子;

global.name = 'javascript' ;
let obj = {
    name : 'obj',
    A    : function(){
        this.name += 'this';
        console.log(this.name)
    },
    B    : function(f){
        this.name += 'this';
        f();
    },
    C    : function(){
      setTimeout(function(){
          console.log(this.name);
      },1000);
    }
}
let a = obj.A;              // 1
a();                        // 2
obj.B(function(){           // 3
    console.log(this.name); // 4
});                         // 5
obj.C();                    // 6
console.log(global.name);   // 7
复制代码

这里列出了三种"丢失"的状况:

  1. 1-2行中obj的A函数赋值给了a,而后调用a(),这时候函数的执行上下文发生了变化,至关因而全局做用域下的一个函数的执行,因此承接咱们上面所说,此时启用了默认绑定
  2. 3-5行中给obj.B传递一个Function参数,而且在Bf()执行,这至关于一个B中的当即执行函数,此时在this所在做用域找不到认领的对象,因而启用默认绑定
  3. 6行是最有意思的一行,为何呢?由于这一行在Node环境浏览器环境下的结果是不同的,按照常理来讲,回调函数中的this一样会由于丢失而启用默认绑定,在浏览器环境下确实如此。可是在node中事情好像没那么简单,咱们先看看输出的结果,在作分析
javascriptthis // 1-2行执行结果
javascriptthis // 3-5行执行结果
javascriptthis // 7行执行结果
undefined      // 6行执行结果
复制代码

你会发现有一个值很扎眼,没错,就是undefined,那为何setTimeout()回调中的this没有启用默认绑定呢?这里根据这篇博客作了一个猜测 : NodeJS 回调函数中的this ,我建议你看一看这篇博客

亦如fs.open()回调同样,setTimeout()函数会先初始化本身,那么此时回调函数做用域上就是存在实例对象了,只是这个对象咱们看不到而已,因此此时this.name并未初始化,因此输出undefined。为此我作了一个实验来证实,setTimeout()this指向不等于global

function A(){
    console.log(this === global);
}
A();  //输出 true
setTimeout(function(){
    console.log(global === this);
},1000);  // 输出 false
复制代码

由此,咱们能够知道,开篇例子中18,19行的输出即是:

21 // 隐式绑定丢失
22 // 箭头函数绑定上级做用域this指针,这个后面会讲
30 //隐式绑定
复制代码

硬绑定(显式绑定)

接下来要讲的是硬绑定 , 这个比较简单,是经过JS的三个经常使用API来显式的实现绑定特定做用域,这三个API为

  • apply
  • call
  • bind

这三个API之间的关系非本篇关键,能够自行了解,本篇以apply为例

咱们知道JS函数的一大特色就是有 定义时上下文运行时上下文 以及 上下文可变 的概念,而apply就是帮助咱们改变函数运行时上下文的环境,这种经过API显式指定某个函数执行上下文环境的绑定方式就是 硬绑定

咱们来看下面这个例子:

global.name = 'global';
function A(){
    console.log(this.name);
}
let obj = {
    name : 'obj'
}
A.apply(obj); //输出 obj
A.apply(global); //输出 global
复制代码

对,你应该懂了,什么叫硬绑定。就是无论函数你定义在哪里,这样使用了我这个API,你就能够随心所欲,绑定到任意做用域的对象上去,哪怕是global都不带怕的,硬核API !!!

由此,你也能够获得开篇例子中20行输出结果应该是:

31  //obj.name在此以前被加了一次1,因此这里是31
复制代码

new操做符绑定

最后一种绑定方式是new操做符绑定,这个也是JS中最经常使用的用法之一了,简单来讲就是经过new操做符来实例化一个对象的过程当中发生了this指针的绑定,这个过程是不可见的,是后台帮你完成了这一绑定过程。具体是什么过程呢?这里咱们就已开篇的例子为例吧

function A(){
    console.log(this.num++);
}
new A(); //输出为 NaN
复制代码

NaN是JSNumber对象上的一个静态属性,意如其名"not a number",表示不是数字。这里new A()实例化了一个对象,此时在A的做用域里就用对象认领this指针了,因此此时this指向实例化对象,可是这个对象中num属性并无初始化,所以是undefined,而undefined非数字却使用了++运算,所以最终输出了NaN

四种绑定的优先级

既然this的绑定有四种机制,那一定会出现机制冲突的状况,不要紧,其实从上面的讲解中你应该已经能隐约感受到这四种机制是有优先级存在的。好比,在new操做符绑定的时候,就是由于new绑定优先级高于默认绑定,因此this指针指向的是新实例化的对象而不是全局对象global。这里给出这四种绑定的优先级 :

 new 操做符绑定   >    硬绑定   >    隐式绑定   >    默认绑定

这个关系仍是挺明显的,故不做例子阐述了。

ES6中引入箭头函数对this的绑定产生了什么影响?


快要结束了,再坚持一下,最后有必要说明如下ES6中的箭头函数对于this指针绑定的影响,ES6中引入箭头函数是为了更优雅的书写函数,对于那些简单的函数咱们使用箭头函数代替原来的函数写法能够大大简化代码量并且看上去更加整洁优雅。也正是由于箭头函数的设计是为了简洁优雅,因此箭头函数除了简化代码表示之外,还简化了函数的行为。

  • 箭头函数不能声明的方式定义,只能经过函数表达式
  • 箭头函数不能经过new来实例化对象
  • 也是由于上面的缘由,箭头函数中并无本身的this指针,这不表明不能使用this,箭头函数中的this是继承自父级做用域上的this,也就是说箭头函数中的this绑定的是父级做用域内this所指向的对象

举个例子来说:

name = 'global';
this.name = 'this';
let obj = {
    name : 'obj',
    A    : function(){
        ()=>{
            console.log(this.name)
        }
    },
    B    :() => {
        console.log(this.name)
    }
}
obj.A(); //输出 obj
obj.B(); //输出 this 
复制代码

这里或许obj.B()输出让你疑惑,其实咱们开篇也讲了,全局做用域下thisglobal风马牛不相及,因此这里对应到父级做用域中this对应的对象就是this自己或者export

由此,开篇示例中18行的输出即可知晓了 :

21 // b() 所输出
22 // (b())()所输出
复制代码

这里有些绕,之因此最终this绑定到了global上,是分了两步

  • 首先,由于是箭头函数,因此this继承父级this绑定到了obj
  • 由于隐式调用的"丢失",致使父级this默认绑定到了global

附上前面程序的输出答案

undefined
undefined
false
20
21
22
30
31
NaN
23
复制代码

总算是写完了,写做的过程,笔者收获也很大,就好比Node中回调函数的this指向问题我也没有想到,是经过实验才印证Node中回调函数中this指向的是自身实例化的对象,这个工做一样不可见,后台完成了,就像new同样。 但愿读者也能够获得收获!

下面是个人微信公众号,若是以为本篇文章你收获很大,能够关注个人微信公众号,我会同步文章,这样能够RSS订阅方便阅读,感谢支持!

相关文章
相关标签/搜索