困扰我 n 久的 this 指向问题 呜呜呜

210 阅读8分钟

本文主要参考 《你不知道的 js》想要了解更多建议自己去看看

什么是执行上下文

  • 相当于是对数据的预处理

  • 执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行

  • 这里再简单说说我对执行上下文和作用于的理解吧 不一定正确希望指正

    • 作用域在声明的时候就已经确定了
    • 而执行上下文呢 重点还是在'执行' 二字吧 前面知道它是对数据做的预处理 那么只有在执行的时候它才会存在 比如 this 指向就是调用才确定的

对 this 的误解

指向自身

  • 很容易把 this 理解成指向函数自身,这个推断从英语的语法角度来说是说得通的。

  • function foo(num) {
      console.log( "foo: " + num );
      // 记录foo被调用的次数
      this.count++;
    }
    foo.count = 0;
    var i;
    for (i=0; i<10; i++)="" {="" if="" (i=""> 5) {
        foo( i );
      }
    }
    // foo: 6
    // foo: 7
    // foo: 8
    // foo: 9
    // foo 被调用了多少次?
    console.log( foo.count ); // 0 
    
    • 如果指向自身 那么因该输出 4 说明并不是指向自身

它的作用域

  • 认为 this 指向函数的作用域 这个问题有些复杂 因为他有时是正确的
  • 需要明确的是 this 任何情况下都不指向函数的词法作用域

this 是什么

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

调用位置

  • 分析函数的调用位置能够更准确的确认 this 的引用

  • 最重要的是 分析调用栈

    • 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 的四种绑定规则

默认绑定

  • 是最常用的函数调用类型:独立函数调用

  • 可以把这条规则看作是无法应用其他规 则时的默认规则

    • function foo() {
        console.log( this.a );
      }
      var a = 2;
      foo(); // 2
    • 这种就是最常见的 this 默认绑定全局对象 window
    • 但是如果是严格模式的话 this 指向是 undefined

隐式绑定

  • 另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含

    • function foo() {
        console.log( this.a );
      }
      var obj = {
        a: 2,
        foo: foo
      };
      obj.foo(); // 2
    • 这里是先声明 foo 然后被当作引用属性添加到 obj 中

    • 但是无论是 直接在obj中定义还是先定义再添加为引用属性,这个函数严格来说都不属于 obj 对象

    • 他保存的只是 foo 的指针

      • 这里要知道 js 默认基本类型保存在栈中 引用类型在堆中 然后栈中保存一个指向堆的指针
    • 隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象 obj 因此 obj.a == this.a

  • 隐式丢失

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

      • 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 就是另一个指向 foo 对象的指针 这个变量 bar 是在全局对象中的 那么 foo 的 this 就变的和默认绑定一样了
      • 虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一 个不带任何修饰的函数调用,因此应用了默认绑定
      • function foo() {
          console.log( this.a );
        }
        function doFoo(fn) {
          // fn其实引用的是foo
          fn(); // <-- 调用位置!
        }
        var obj = {
          a: 2,
          foo: foo
        };
        var a = "oops, global"; // a是全局对象的属性
        doFoo( obj.foo ); // "oops, global"
        
      • 这里是以回调函数的形式传入 这里调用的其实也是 foo 本身了
      • 参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子 一样

显示绑定

  • 在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属 性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象上

  • 那么如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?

  • 可以使用内置的几个方法来实现显示绑定

    • function foo() {
      console.log( this.a );
      }
      var obj = {
      a:2
      };
      foo.call( obj ); // 2
      
    • 对比call 、bind 、 apply 传参情况下

      • var name = '小王', age = 17
        var obj = {
          name: '小张',
          objAge: this.age,
          myFun: function() {
            console.log(this.name + '年龄' + this.age, '来自' + fm + '去往' + t)
          }
        }
        ​
        var bd = {
          name: 'dlow',
          age: 99
        }
        ​
        obj.myFun.call(db,'成都','上海');     // 德玛 年龄 99  来自 成都去往上海
        obj.myFun.apply(db,['成都','上海']);      // 德玛 年龄 99  来自 成都去往上海  
        obj.myFun.bind(db,'成都','上海')();       // 德玛 年龄 99  来自 成都去往上海
        obj.myFun.bind(db,['成都','上海'])();   // 德玛 年龄 99  来自 成都, 上海去往 undefined
        
      • call 、bind 、 apply 这三个函数的第一个参数都是 this 的指向对象,第二个参数差别就来了:

      • call 的参数是直接放进去的,第二第三第 n 个参数全都用逗号分隔,直接放到后面 obj.myFun.call(db,'成都', ... ,'string' )

      • apply 的所有参数都必须放在一个数组里面传进去 obj.myFun.apply(db,['成都', ..., 'string' ])

      • bind 除了返回是函数以外,它 的参数和 call 一样。

        当然,三者的参数不限定是 string 类型,允许是各种类型,包括函数 、 object 等等!

硬绑定
  • 但是显示绑定无法解决隐式丢失的情况

  • 可以使用变种 硬绑定

    • 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 他总是会手动在 obj 上调用 foo 这就是硬绑定

    • 硬绑定的应用场景就是创建一个包裹函数,传入所有的参数并返回接收到的所有值:

      • function foo(something) {
          console.log( this.a, something );
          return this.a + something;
        }
        var obj = {
          a:2
        };
        var bar = function() {
          return foo.apply( obj, arguments );
        };
        var b = bar( 3 ); // 2 3
        console.log( b ); // 5
        
  • 另一种使用方法是创建一个可以重复使用的辅助函数:

    • function foo(something) {
        console.log( this.a, something );
        return this.a + something;
      }
      // 简单的辅助绑定函数
      function bind(fn, obj) {
        return function() {
          return fn.apply( obj, arguments );
        };
      }
      var obj = {
        a:2
      };
      var bar = bind( foo, obj );
      var b = bar( 3 ); // 2 3
      console.log( b ); // 5
    • 所以内置的 bind 方法就实现了这个功能

    • bind 会返回一个硬绑定的新函数 会把参数设置为 this 的上下文并调用原始函数

      • function foo(something) {
          console.log( this.a, something );
          return this.a + something;
        }
        var obj = {
          a:2
        };
        var bar = foo.bind( obj );
        var b = bar( 3 ); // 2 3
        console.log( b ); // 5

new 绑定

  • 使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作

    • 1.创建一个新对象

    • 2.将新对象的 proto 指向构造函数的 prototype 属性

    • 3.将构造函数的 this 指向新对象

    • 4.执行构造函数内部的代码

    • 5.将新对象返回

      • let objA = (function {
          let obj = {}
          obj._proto_ = CreateObj.prototype
          CreateObj.call(obj, 'A')
          return obj
        })()
        console.log(objA.name)
        

优先级

  • 判断this 可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的顺序来 进行判断:
    1. 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。

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

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

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

      var bar = foo()
      

补充

  • 箭头的函数没有自己的this,this是靠包住 箭头函数的普通函数给的

    • 1.找普通函数
    • 2.这个普通函数要包住箭头函数
    • 3.一个误区就是很多人认为 {} 内部的就是作用域了 但是 对象的 {} 不属于块级作用域 那么他会继续向上层查找 很可能根据作用域链查找到 window