js中的this学习

在以前的对象原型的文章中,咱们讲到了在函数前面加new而后进行调用以后发生的4件事情,当时只把跟原型有关的东西介绍了一下。如今咱们来学习一下其余的内容。数组

首先先来回顾一下会有哪四件事情发生吧:app

  • 一个全新的对象被建立
  • 这个新的对象会被接入到原型链
  • 这个新的对象被设置为函数调用的this绑定
  • 除非函数有返回对象,不然这个被new调用的函数将自动返回这个新的对象

这里有个新的东西this绑定,也就是接下来咱们要介绍的东西啦。函数

第一个问题就是this是什么?(如下回答摘自You-Dont-Know-JS)工具

this是在函数被调用的时候创建的一个绑定,指向的内容彻底由函数被调用的调用点来决定的。

简单点说,this就是一个绑定,指向一个内容。学习

那么this指向的内容又是什么呢?前面说到这个内容由函数被调用的调用点来决定。所谓的调用点,就是函数在代码中被调用的地方。也就是说,咱们须要找到函数在哪里被调用,从而肯定this指向的内容。考虑这个问题还须要了解一个概念:调用栈(到达当前执行位置而被调用的全部方法的堆栈)。
看段代码来深刻理解一下调用栈和调用点这两个概念:this

function foo() {
    // 调用栈是: `foo`
    // 调用点是global scope(全局做用域)
    console.log( "foo" );
    bar(); // <-- `bar`的调用点
}
function bar() {
    // 调用栈是: `foo` -> `bar`
    // 调用点位于`foo`
    console.log( "bar" );
    baz(); // <-- `baz`的调用点
}
function baz() {
    // 调用栈是: `foo` -> `bar` -> `baz`
    // 调用点位于`bar`
    console.log( "baz" );
}
foo(); // <-- `foo`的调用点

上面这个代码跟注释应该已经很清楚了解释了调用栈和调用点这两个概念了。prototype

搞清楚这些概念以后,咱们仍是不知道this会指向什么。既然说this指向的内容彻底由调用点决定,那么调用点又是怎么决定的呢?code

还记得文章最开始提到的东西么,关于new的4件事情,第三点讲的是新对象被设置为函数调用的this绑定。
看下代码:对象

function foo(){
      this.a = a;
  }
  var bar = new foo(2); //调用foo函数来建立一个新对象bar
  console.log(bar.a);

使用new来调用函数foo的时候,咱们建立了一个新对象bar而且把bar绑定到了foo()里面的this.这就是所谓的new绑定。ip

那么在JavaScript中,关于this绑定,除了new绑定,还有3种其它的规则:

  • 默认绑定
  • 隐式绑定
  • 显示绑定

下面咱们依次来一一介绍。

  1. 默认绑定
    看名字咱们就能看出来,这是最普通最基础的绑定。通常来讲,独立函数调用的时候this就是默认绑定。
    来看个例子:

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

    代码很简单,咱们主要关心的是this。咱们先看结果:this绑定到了全局变量。
    具体分析一下也很简单,这里的函数调用就是咱们日常在使用的最简单的独立函数的调用,跟前面介绍的规则也很符合。
    这里有一个要注意的小细节就是若是是在严格模式下,默认绑定的值会变成undefined。若是是非严格模式的话,就是绑定到全局变量了。

  2. 隐式绑定
    这个规则通常是看函数调用的位置是否有上下文对象,或者说是否被某个对象拥有或者包含。
    经过代码来深刻理解一下:

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

    代码一样很好理解,函数foo()做为引用属性添加在对象obj里面,但这并不能说明函数foo()属于obj对象。可是从调用的位置上看,会使用obj这个对象来引用函数foo,那么函数在被调用的时候,是被obj这个对象拥有或者包含的。
    简单点说,函数在被调用的时候,是经过对象来引用的,那么函数里的this就会绑定到这个对象上面。
    再来看一个稍微复杂一点的例子:

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

    这里的话,咱们会发现多了一个obj1这个对象,并且这个对象里有属性a和对象obj。而后咱们调用的时候会发现结果输出的是obj里面的属性a的值。
    简单的结论就是,在多层的对象引用属性中,只有最顶层或者说最后一层才会影响调用位置。

  3. 显式绑定
    经过上面隐式绑定的规则介绍能够知道,它是经过对象间接绑定this的,那么很明显显式绑定就是直接的,或者说就是强行指定咱们想要让this绑定的对象。那么怎么来进行强行绑定呢?
    通常来讲,是使用函数的call()和apply()方法(绝大部分函数都会有这两个方法)。
    这两个方法的做用都是同样的,就是替换this指向。惟一不一样的就是接收参数的方法不同。apply()方法接收两个参数,第一个参数是一个对象(也就是咱们想要让this指向的新的对象,不填的话就是全局对象),第二个参数一个参数数组。call()方法的话第一个参数跟apply是同样的,可是后面要把传递的参数所有都列举出来。
    简单来看个例子:

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

    最后一行代码,函数foo调用了call方法,强行把this绑定到了obj对象上。

至此,关于this绑定的基础的4种规则就介绍得差很少了,实际上有些规则在应用的时候可能不那么尽如人意,咱们依旧从代码入手:

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

一开始咱们可能都会以为输出的结果应该是2。由于bar这个对象在建立的时候调用了obj里面的foo函数。但实际上只是另外一个foo本身的引用。并且bar函数在调用的时候是做为独立函数调用的,跟咱们前面讲的默认绑定的规则很符合,因此这里的this就绑定到了全局对象。
这种状况在回调函数里更容易发生,好比下面的代码:

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

最后一行代码实际上就是f = obj.foo,天然结果就跟上面是同样的。
那么有什么方法能够解决这个问题呢?
在显示绑定中,有一个它的变种,咱们称之为硬绑定,能够解决上面的问题。
继续看代码:

function foo(){
     console.log(this.a);
 }
 var obj = {
    a:2
 };
 var bar = function(){
    foo.call(obj);
 }
 bar(); //2
 setTimeout(bar,1000);
 bar.call(window); //2

这段代码解释了硬绑定的工做原理:它建立了一个函数bar,而后在函数里面经过foo.call(..)强行把this绑定到了obj对象上面。以后只要调用函数bar,就会调用函数foo,绑定的值始终不变。

而后咱们稍微改变一下,让它变成一个可复用的帮助函数:

function foo(){
     console.log(this.a);
 }
 function bind(f,obj){
     return function(){
         return f.apply(obj,arguments);
     };
 }
 var obj = {
    a:2
 };
 var bar = bind(foo,obj);
 var b = bar(3);  
 console.log(b);  //2

因为硬绑定常常被使用,因此它在ES5的时候就做为内建工具了:Function.prototype.bind。上面的代码就是bind方法的原理。
bind方法的做用和call和apply同样,都是替换this指向,它的参数也和call同样。不同的就是bind方法返回的是一个函数。

而后咱们要介绍一个比较特殊的函数,由于它不能根据前面介绍的4条规则来判断this的指向。就是ES6中新增的函数:箭头函数(=>)。它是根据外层做用域或者全局做用域来决定this指向的。
看段代码:

function foo(){
    return (a) => {
        console.log(this.a);
    };
}
var obj1 = {
    a:1
};
var obj2 = {
    a:2
};
var bar = foo.call(obj1);
bar.call(obj2);//1

foo()内部建立的箭头函数会捕获调用时foo()的this。由于foo使用了call方法,因此foo()的this绑定到了obj1。而后bar对象被建立的时候引用了箭头函数,因此bar的this也被绑定到了obj1上面。并且箭头函数的绑定是没法被修改的。因此最后输出的结果是1而不是2。

最后,虽然咱们已经了解了this绑定的基本规则,可是若是说咱们找到了函数在哪里调用,而后又发现4种规则里有多种规则能够适用,那咱们应该选择哪种呢?

这就涉及到了这些规则的优先级:

  1. 首先看是否是有new调用,若是是的话就绑定到新建立的对象;
  2. 而后看是否是有call或者apply或者bind调用,若是是那就绑定到指定对象;
  3. 再以后看是否是由上下文调用,若是是就绑定到那个上下文对象;
  4. 最后的话就只剩下默认绑定了(注意严格模式下是undefined,非严格模式下绑定到全局对象)。
相关文章
相关标签/搜索