• 关于this
    this被自动定义在所有函数的作用域中。this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。当一个函数被调用时,会创建一个活动记录(也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。

  • 对this的误解

    1. this指向函数自身
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      function foo(num){
      console.log('foo:' + num);
      this.count++;
      }
      foo.count = 0;
      var i;
      for (i =0; i < 10; i++) {
      if (i > 6) {
      foo(i)
      }
      }
      // foo:7
      // foo:8
      // foo:9
      console.log(foo.count) // 0

    上面的代码中,我们使用foo.count来记录函数foo被调用的次数,每次调用foo的时候count的值就加1。但是在循环结束之后我们打印函数foo的count属性发现还是0。这和我们想象的不一样。这是因为此时的函数是在全局作用域下被调用的,这时this指向的是全局作用域,并不是指向函数foo自身。

    1. this指向函数的作用域
      this指向函数的作用域是第二个常见的误解。this在任何情况下都不指向函数的词法作用域。
  • this解析
    在上面的讲解中我们知道每个函数的this是在调用时(运行时)被绑定的,完全取决于函数的调用位置

    • 调用位置
      所谓的调用位置就是函数在代码中被调用的位置(不是声明的位置)。想要准确的寻找到函数的调用位置,我们需要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。而调用位置就在当前正在执行的函数的前一个调用中
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      function baz(){
      // 调用栈为baz
      // 当前的调用位置是全局作用域
      console.log('baz')
      bar() // bar的调用位置
      }
      function bar(){
      // 调用栈为baz->bar
      // 当前的调用位置是baz
      console.log('bar')
      foo() // foo的调用位置
      }
      function foo(){
      // 调用栈为baz->bar->foo
      // 当前的调用位置是bar
      console.log('foo')
      }
      baz() // baz的调用位置:全局作用域
  • this绑定规则
    在理解了上面的调用位置之后,我们来看看this的绑定规则,在找到this所绑定的对象之前,必须先找到函数的调用位置,然后在判断适用于那条绑定规则。

    1. 默认绑定
      在非严格模式下,this的默认绑定指向的是全局对象。如果在严格模式下this将会绑定到undefined。注意⚠️虽然this的绑定规则完全取决于调用位置,但是只有运行在非严格模式下时,默认你绑定才能绑定到全局对象;在严格模式下调用函数则不会影响默认绑定

      1
      2
      3
      4
      5
      function foo(){
      console.log(this.a)
      }
      var a = 2
      foo() // 2; 在全局作用域下调用foo,所以this采用的是默认绑定,此时this指向的是全局作用域

      下面我们看看函数运行在严格模式下的情况

      1
      2
      3
      4
      5
      6
      function foo(){
      'use strict' // foo运行在严格模式下
      console.log(this.a)
      }
      var a = 2
      foo() // VM500:3 Uncaught TypeError: Cannot read property 'a' of undefined;此时this绑定的是undefined

      接下来我们看看函数运行在非严格模式下,但是在严格模式下调用的情况

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      function foo(){
      // foo运行在非严格模式下
      console.log(this.a)
      }
      var a = 2
      (function(){
      'use strict'
      // foo在严格模式下被调用
      foo() // 2 ;虽然在严格模式下调用,但是不影响this的默认绑定为全局作用域
      })()
    2. 隐式绑定
      隐式绑定的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      function foo() {
      console.log(this.a)
      }

      var obj = {
      a: 2,
      foo: foo
      }
      obj.foo() // 2

      在上面的代码中,我们先声明了一个函数foo,然后声明了一个对象obj,在obj中我们的一个属性foo引用了foo函数,但是无论我们是直接在obj对象中定义foo函数,还是像上面的先定义然后再引用,这个函数严格来说都不属于obj对象。但是函数的调用位置会使用obj上下文来引用函数,因此这时候可以说函数被调用时obj对象拥有或者包含它。
      因此,当函数引用有上下文对象时,隐士绑定规则会把函数调用中的this绑定到这个上下文对象。
      对象属性链中只有上一层或者说最后一层在调用位置中起作用.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      function foo(){
      console.log(this.a)
      }
      var obj1 = {
      a:1,
      foo: foo
      }
      var obj2 = {
      a:2,
      obj1:obj1
      }
      obj2.obj1.foo() // 1此时this绑定的是对象属性链中的最后一层也就是obj1

      -. 隐式丢失
      最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      function foo(){
      console.log(this.a)
      }
      var obj = {
      a:2,
      foo:foo
      }
      var bar = obj.foo // obj引用foo,并且将其赋值给bar属性
      var a = 'global'
      bar() // 'global'

      在上面的代码中,bar引用的是foo函数本身,所以调用bar函数会将foo中的this采用默认绑定,从而绑定到全局作用域.
      再来看看下面的代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      function foo(){
      console.log(this.a)
      }
      function doFn(fn){
      fn() // foo的调用位置
      }
      var obj = {
      a:2,
      foo: foo
      }
      var a = 'gloabl'
      doFn(obj.foo) // 'gloabl'

      在上面的例子中,我们的foo函数被当成函数的参数传递,其实也是传递的函数的引用,所以也会将this绑定到全局作用域中,另外比如在全局作用域中调用setTimeout(fn,1000)函数时,如果fn中存在this的话,那么其也会被绑定到全局作用域中。

    3. 显示绑定
      js中显示绑定采用的是call和apply方法。

      • call和apply
        他们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this。因此可以直接指定this的绑定对象,称之为显示绑定。

        1
        2
        3
        4
        5
        6
        7
        function foo(){
        console.log(this.a)
        }
        var obj = {
        a: 2
        }
        foo.call(obj) // 2
        • 装箱
          如果你传入了一个原始值(字符串、布尔类型或者数字类型)来当作this的绑定对象,这个原始值会被转换成它的对象形式。这就叫做装箱。
        • 硬绑定
          显示绑定和隐式绑定一样无法解决this绑定丢失的问题,但是我们通过在绑定外部创建一个函数,并在该函数内部强制将this绑定到我们需要的对象上,这个就是硬绑定。

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          function foo(){
          console.log(this.a)
          }
          var obj = {
          a: 2
          }
          var bar = function(){
          foo.call(obj)
          }
          bar() // 2
          setTimeout(bar, 1000) // 2
          bar.call(window) // 2

          如上所示,我们将foo的this绑定封装在匿名函数中,并将该函数赋值给bar变量,这样无论外部如何传入参数都无法改变this的绑定对象。

        • 另外一种创建硬绑定的方式是创建一个可以重复使用的辅助函数
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          // 辅助函数
          function bind(fn, obj){
          return function(){
          return fn.apply(obj, arguments)
          }
          }
          function foo(num){
          console.log(this.a, num)
          return this.a + num
          }
          var obj = {
          a: 2
          }
          var bar = bind(foo, obj)
          bar(3) // 5

        另外ES5提供了内置的硬绑定bind方法,bind方法返回一个硬编码的新函数,它会把你的参数设置为this的上下文并调用原始函数。

    • new绑定

      1. js的new操作符
        在js中的构造函数只是使用new操作符时被调用的函数,他们并不会属于某个类,也不会实例化一个类。实际上它们只是被new操作符调用的普通函数而已。
        使用new来调用函数时会发生以下的几个步骤

        1. 创建一个全新的对象
        2. 这个新对象会被执行[[prototype]]连接
        3. 这个新对象会绑定到函数调用的this
        4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
          1
          2
          3
          4
          5
          function Foo(a){
          this.a = a
          }
          var foo = new Foo(2)
          foo.a // 2

        使用new来调用函数时,会构造一个新对象并把它绑定到foo调用中的this上。

    • 通过绑定优先级来判断this
      通过优先级来判断this有以下规则:

      1. 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。

        1
        var bar = new Foo()
      2. 函数是否通过call、apply(显示绑定)或者硬绑定来调用?如果是的话,this绑定的是指定的对象。

        1
        var bar = foo.call(obj)
      3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。

        1
        var bar = obj1.foo()
      4. 如果以上都不是的话,则采用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。

        1
        var bar = foo()

    凡事都有例外,绑定也是如此;在某些场景下,this的绑定行为会出乎意料

    1. 如果把null和undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际上应用的是默认绑定规则。即是全局对象或则会是undefined。

      1
      2
      3
      4
      5
      function foo(){
      console.log(this.a)
      }
      var a = 3
      foo.call(null) // 3

      传入null或者undefined的情况:如果一个函数并不关心this的话,你仍然需要传入一个占位符,这时候null或者undefined是一个不错的选择。

    2. 间接引用:当创建一个函数的间接引用(函数赋值)的时候,调用函数会应用默认绑定规则

      1
      2
      3
      4
      5
      6
      7
      function foo(){
      console.log(this.a)
      }
      var a = 2
      var obj = {a: 3, foo: foo}
      var otherObj = {a: 4}
      (otherObj.foo = foo.foo)() // 2

      以上otherObj.foo是指向foo的引用,所以otherObj.foo被调用相当于foo直接在全局作用域中执行。
      注意⚠️:对于默认绑定来说,决定this绑定对象的并不是函数调用位置是否处于严格模式,而是被调用的函数体内是否处于严格模式。如果函数体内处于严格模式,this会被绑定到undefined,否则this会被绑定到全局对象。

    3. 软绑定
      软绑定是相对于硬绑定来说的,硬绑定这种方式可以把this潜质绑定到指定的对象(除了使用new调用函数),是为了防止函数调用应用默认绑定规则。但是这样却导致以后没有办法修改this的绑定对象,所以我们可以采用软绑定来实现修改this的绑定对象。(类似于ES5内置的dind的实现)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      if (!Function.prototype.softBind) {
      Function.prototype.softBind = function(obj){
      var fn = this
      // 获取到所有的参数
      var curried = [].slice.call(arguments, 1)
      var bind = function(){
      // 如果this不存在或者this采用了默认绑定的话,那么则强制将this绑定到指定的obj对象中,否则不修改this的绑定.此this和上面的this不一样。
      return fn.apply((!this || this === window)? obj : this, curried.concat.apply(curried, arguments))
      }
      bind.prototype = Object.create(fn.prototype) // 将fn的原型赋值给bind的原型是为了能在bind中调用fn原型上的方法或者属性
      return bind
      }
      }
  • this词法
    以上规则适合于普通的常规函数,但是对于ES6中的箭头函数中的this则不使用。
    箭头函数是根据外层(函数或者全局)作用域来决定this的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function foo(){
    // 返回一个箭头函数
    return (a) => {
    // 此时箭头函数内部的this是继承于foo(),因此foo中的this也就是箭头函数的this
    console.log(this.a)
    }
    }
    var obj1 = {
    a: 2
    }
    var obj2 = {
    a: 3
    }
    var bar = foo.call(obj1) // 将foo内部的this通过显示绑定到obj1中,所以箭头函数中的this指向的也是obj1
    bar.call(obj2) // 2。虽然是通过显示绑定想修改this的绑定,但是箭头函数中的this是根据外层的作用域来决定的,也就是foo的作用域。