前端边角料 | 浅析 JavaScript 中 this 的理解

457 阅读10分钟

关键词:this bind call apply

背景

经常在工作中、面试中用到关于 this 的理解,有时候会产生不少 bug 。所以想好好梳理下在 JavaScript 这门语言中,this 究竟是怎样的一种存在,以及自己怎么理解更合适。

目录

  • 个人的理解
  • 较为权威的理解
  • 实际场景的应用

个人的理解

此处均为个人的理解,仅供参考。

this 是什么

在我看来,this 其实就跟它的中文意思一样,一个指代词,“这个”的意思。对应到我们的代码中,比如 this.funcA(),就可以理解为“将‘这个’的方法调用一下”,其中 this 对应 这个. 对应 funcA() 对应 方法funA() 调用。

在代码中,其实就是一个“执行上下文”的指代,即在“当前执行上下文”中的某个方法的调用。草率一点,可以理解为this即代表了“上下文”。

这个就是我对 JavaScript 中 this 的理解,在对比其他语言,比如 Java 语言时,发现 this 的意义又完全不同了。

在 Java 中,this 其实就是一个类的实例的替代。比如 User user = new User() 的实例化中,如果User 类中用了 this,那可以理解为,在其内部的 this 就是这个对象 user 的指代。

tips: 要注意的是,上述表达,仅是为了方便理解 this 的作用,为了保证代码的严谨,建议去官方文档中查询完整的定义.

使用 this 的注意点

有了上述的理解,其实也可以很好的说明为什么在 JavaScript 中 this 经常给开发者造成困扰了。在实际应用,只需要记住以下几个场景的应用即可。

  • 在绝大多数情况下,函数的调用方式决定了 this 的值,即this到底指向(引用)哪个对象必须到函数被调用时才能确定。
  • 这也就是我们常说的谁调用,指向谁
  • 绑定规则:
    • 默认绑定(非严格模式为 window,严格模式为 undefined
    • 隐式绑定
    • 显示绑定(apply,call,bind)
    • new 绑定
    • 箭头函数

较为权威的理解

在对比了 MDN 、《JavaScript 高级程序设计(第4版)》、《你不知道的 JavaScript(上卷)》之后,认为后者解释的更全面,所以做了摘抄。

为什么要用 this

this 提供了一种隐式“传递”对象引用的方式,可以将代码中的 API 设计的更加简洁并且易于复用。

this 到底是什么

this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

this 的绑定规则

在函数执行过程中调用位置是如何决定 this 的绑定对象。

  • 默认绑定

    • 说明
      • 独立函数调用,当 this 绑定无法应用其他规则时的默认规则。
      • 非严格模式下,函数的默认绑定,this 指向的是全局对象,浏览器中一般指 window。
      • 严格模式下,函数的默认绑定,this 指向的是 undefined。
    • 示例
    // 非严格模式
    function foo() {
        console.log(this.a);
    }
    var a = 2;
    foo(); //输出 2, 此时可以视为 window.foo() 的调用,所以 this 指向 window
    
    // 严格模式
    function zoo(){
        'use strict'
        console.log(this.b)
    }
    var b = 3;
    zoo(); // 输出 TypeError : this is undefined,此时依然可以视为 window.zoo(),但是由于是严格模式,所以 this 没有办法默认指向为 window 
    
  • 隐式绑定

    • 说明
      • 调用位置是否有上下文,或者说是否被某个对象拥有或者包含。当函数引用有上下文时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。
      • 对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。
      • 隐式绑定丢失:一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式。
      • 回调函数中丢失 this 绑定也是常见的,比如 setTimeout(fn,delay) 函数,经常会出现在 fn 方法中无法正常使用 this 的情况
    • 示例
      • 隐式绑定
      function foo(){
          console.log(this.a)
      }
      var obj = {
          a:2,
          foo:foo
      }
      var a = 3;
      obj.foo(); // 输出 2,此时 foo 的上下文为 obj,所以 this 指向的也是 obj
      
      // 多层级对象属性引用链示例
      function getName(){
          console.log(this.name)
      }
      var child = {
          name: 'zhang san',
          getName: getName
      }
      var parent = {
          name: 'zhang fu',
          child: child
      }
      parent.child.getName(); // 输出 zhang san ,此时调用链的最后一层对象是 child,所以获取的 this 指向是 child 对象
      
      • 隐式绑定丢失
      function foo(){
          console.log(this.a)
      }
      var obj = {
          a:2,
          foo:foo
      }
      var bar = obj.foo; 
      var a = 3;
      bar(); // 输出 3,因为此时的 bar 是 foo 的引用,但是在调用 bar() 时,其上下文并不是在对象 obj 上,而是在 window 上,如果此时是严格模式,则 this 为 undefined
      
      • 回调函数中的隐式绑定丢失
      function foo(){
          console.log(this.a)
      }
      var obj={
          a:2,
          foo:foo
      }
      var a = 3;
      setTimeout(obj.foo,100) // 输出 3,因为在 setTimeout(callback,delay) 的 callback 中,传入本质还是 foo 方法本身,当执行这个 callback 时,其上下文并不是 obj 对象,而是默认模式下的绑定,即 window 
      
  • 显式绑定

    • 说明:使用 call、apply、bind 方法可以直接给 this 制定绑定的目标对象(此处不详细说明 call、apply、bind 的使用方法)。
    • 显式绑定示例
    // 此处仅举例 call
    function foo(){
        console.log(this.a);
    }
    var obj = {
        a:2
    }
    var a = 3;
    foo.call(obj) // 输出2,如果没有使用 call,则会输出 3,call 强制将 this 指向了 obj,针对 apply、bind 作用相似,不做额外举例
    
    • 解决隐式绑定丢失问题
     function foo(){
            console.log(this.a)
        }
    var obj={
        a:2,
    }
    var a = 3;
    var bar = function(){
        foo.call(obj)
    }
    setTimeout(bar,100) // 输出 2,因为此时 bar 不再是单纯的调用一个 foo(),而是强制将 foo 绑定到了 obj 上,所以解决了隐式绑定丢失的问题
    
    bar.call(window) // 输出 2,硬绑定的 bar 不可能再修改它的 this
    
  • new 绑定

    • 说明
      • 在 JavaScript 中,构造函数只是一些使用 new 操作符时被调用的函数。它们不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已。
      • 一个重要但是非常细微的区别:实际上并不存在所谓的“构造函数”,只是对于函数的“构造调用”。
      • 使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:
        • 创建(或者说构造)一个全新的对象
        • 这个新对象会被执行 [[Prototype]] 连接
        • 这个新对象会绑定到函数调用的 this
        • 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象
    • 代码示例
    function foo(a){
        this.a = a
    }
    var bar = new foo(2)
    var a = 3
    console.log(bar.a) // 输出2,因为此处使用了 new 对象,this 会指向 new foo() 默认返回的对象
    
  • 优先级

    • 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象
    • 函数是否通过 call、apply、bind调用(显示绑定)?如果是的话,this 绑定的就是指定的对象
    • 函数是否在某个上下文对象中引用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象
    • 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到全局对象
  • 绑定例外

    • 如果 call、apply、bind 传入的是 null,则实际应用的是默认绑定规则
    • 间接引用时,也会应用默认绑定,代码示例如下
        function foo(){
            console.log(this.a)
        }
        var a = 2;
        var o = {
            a:3,
            foo:foo
        }
        var p = {
            a:4
        }
        o.foo() // 输出 3,正常的隐式绑定
        (p.foo = o.foo)() // 输出 2,此时赋值表达式返回的值是 foo() 方法的引用,所以在调用时,就是默认绑定
    
    • 箭头函数 ()=>{} : ES6 中的箭头函数并不会使用上述4条规则,而是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。

实际应用的场景

在实际开发中,有很多场景会用到 this,而在我们最熟悉的两个框架中,也有一些比较明显的 this 指向相关的问题,我在这里简单做一个举例。

Vue 框架中的注意点

在 Vue 的很多地方,都不应该使用箭头函数,比如最常用的 methods API 中,官方有明确说明:methods 将被混入到 Vue 实例中。可以直接通过 VM 实例访问这些方法,或者在指令表达式中使用。方法中的 this 自动绑定为 Vue 实例。

注意,不应该使用箭头函数来定义 method 函数 (例如 plus: () => this.a++)。理由是箭头函数绑定了父级作用域的上下文,所以 this 将不会按照期望指向 Vue 实例,this.a 将是 undefined。

从代码示例中就可以看出为什么不可以用箭头函数:

var vm = new Vue({
  data: { a: 1 },
  methods: {
    plus: function () {
      this.a++
    }
  }
})
React 框架中的注意点

你必须谨慎对待 JSX 回调函数中的 this,在 JavaScript 中,class 的方法默认不会绑定 this。如果你忘记绑定 this.handleClick 并把它传入了 onClick,当你调用这个函数的时候 this 的值为 undefined。

这并不是 React 特有的行为;这其实与 JavaScript 函数工作原理有关。通常情况下,如果你没有在方法后面添加 (),例如 onClick={this.handleClick},你应该为这个方法绑定 this。

如果觉得使用 bind 很麻烦,这里有两种方式可以解决。如果你正在使用实验性的 public class fields 语法,你可以使用 class fields 正确的绑定回调函数:

class LoggingButton extends React.Component {
  // 此语法确保 `handleClick` 内的 `this` 已被绑定。
  // 注意: 这是 *实验性* 语法。
  handleClick = () => {
    console.log('this is:', this);
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}

如果你没有使用 class fields 语法,你可以在回调中使用箭头函数:

class LoggingButton extends React.Component {
  handleClick() {
    console.log('this is:', this);
  }

  render() {
    // 此语法确保 `handleClick` 内的 `this` 已被绑定。
    return (
      <button onClick={() => this.handleClick()}>
        Click me
      </button>
    );
  }
}

此语法问题在于每次渲染 LoggingButton 时都会创建不同的回调函数。在大多数情况下,这没什么问题,但如果该回调函数作为 prop 传入子组件时,这些组件可能会进行额外的重新渲染。我们通常建议在构造器中绑定或使用 class fields 语法来避免这类性能问题。

杂谈

  • 作为面向对象的弱语言类型的动态语言,JavaScript 并没有面向对象的强语言类型的静态语言 Java 那样严谨。
  • 在寻找 this 相关的资料中,有一个体会就是,之所以自己会搞混,其实还是因为把 JavaScript 的 this 和其他面向对象语言的 this 理解相匹配了。
  • 给我印象最深的,还是关于 JavaScript 语言的 new 语法的说明,即在 JavaScript 并没有如同传统面向对象语言的类的实现。
  • 在找资料的过程中还有一个感悟就是,一定要多看,多找,不要仅去看某一篇文章、某一本书,等你都找一遍之后,你才会发现,究竟谁的观点更让人容易理解。

参考资料

浏览知识共享许可协议

知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。