• 发生时间:2022-08-22

  • 问题描述:完成上位机客户端模块的开发后(代码片段如下所示),运行时报错:TypeError: Cannot read properties of undefined (reading '_isDebugMode')

      class Client {
      _startRecvFrame() {
        port.on('data', this._recvFrame)
      }
    
      _recvFrame(frame) {
        if (this._isDebugMode) {
          console.log(xxx)
        }
      }
    }
    
  • 问题类别:软件开发

  • 原因分析:

    1. 根据报错信息,可以推断:调用 _recvFrame 时,函数内的 this 没有绑定到 Client 对象,因此其取值为 undefined
    2. 查阅 JS this 关键字 的相关资料,发现 JS this 的绑定一共有 4 条规则
    • 规则1:默认绑定(Default Binding)。即 this 默认不绑定任何对象,其取值为 undefined
    • 规则2:隐式绑定(Implicit Binding)。即如果函数被调用时含有上下文对象,就绑定到该对象。如 obj.func()func 里面的 this 绑定到 obj
    • 规则3:显式绑定(Explicit Binding)。即如果函数是以 bind / call / apply 的方式被调用时,就绑定到传入的对象。如 method.bind(obj)method 里面的 this 绑定到 obj
    • 规则4:new 绑定 (new Binding)。即如果函数是以 new 的方式被调用时,就绑定到返回的对象。如 var bar = new foo(2)foo 里面的 this 绑定到 bar
    1. 由于 _recvFrame 是以回调函数的方式被调用的,在调用时已经失去原上下文对象 Client (参考Implicitly Lost),这里要应用规则1,导致出现问题。
  • 解决方案:

    1. 解决这个问题有两种方案:
    • 方案一:改用箭头函数
    • 方案二:改用显式绑定
    // 方案一:改用箭头函数
    
    class Client {
      _startRecvFrame() {
        port.on('data', (frame) => {
          this._recvFrame(frame)
        })
      }
    }
    
    // 方案二:改用显式绑定
    class Client {
      _startRecvFrame() {
        port.on('data', this._recvFrame.bind(this)(frame))
      }
    }
    
    1. 我采用了方案一.
  • 实施结果:按照方案一修改代码后,运行程序不再报错。

  • 经验总结:

    1. JS 的 this 关键字是个容易踩坑的地方,特别是在有回调函数的时候。此时记住 JS this 绑定的 4 条规则 可以救命。
    2. You-Dont-Know-JS 是一份深入了解 JS 的好资料(第 2 版还没写完,可以先看第 1 版),JS 开发者应该学习一下。

《You Don’t Know JS: this & Object Prototypes》部分要点摘录

Call-site

  • To understand this binding, we have to understand the call-site: the location in code where a function is called (not where it’s declared).
  • Take care when analyzing code to find the actual call-site (from the call-stack), because it’s the only thing that matters for this binding.

Lexical this

  • Instead of using the four standard this rules, arrow-functions adopt the this binding from the enclosing (function or global) scope.

Review(TL;DR)

Determining the this binding for an executing function requires finding the direct call-site of that function. Once examined, four rules can be applied to the call-site, in this order of precedence:

  1. Called with new? Use the newly constructed object.
  2. Called with call or apply (or bind)? Use the specified object.
  3. Called with a context object owning the call? Use that context object.
  4. Default: undefined in strict mode, global object otherwise.