【问题标题】:Typescript: can not access member value in inherited class constructor打字稿:无法在继承的类构造函数中访问成员值
【发布时间】:2018-09-21 08:44:59
【问题描述】:

我有一个类A,以及一个继承自它的类B

class A {
    constructor(){
        this.init();
    }
    init(){}
}

class B extends A {
    private myMember = {value:1};
    constructor(){
        super();
    }
    init(){
        console.log(this.myMember.value);
    }
}

const x = new B();

当我运行此代码时,我收到以下错误:

Uncaught TypeError: Cannot read property 'value' of undefined

我怎样才能避免这个错误?

对我来说很清楚,JavaScript 代码会在创建 myMember 之前调用 init 方法,但应该有一些实践/模式才能使其工作。

【问题讨论】:

    标签: javascript typescript oop inheritance ecmascript-6


    【解决方案1】:

    这就是为什么在某些语言(C#)中,代码分析工具会标记构造函数中虚拟成员的使用情况。

    在 Typescript 中,字段初始化发生在构造函数中,在调用基本构造函数之后。字段初始化写在字段附近的事实只是语法糖。如果我们查看生成的代码,问题就会变得清晰:

    function B() {
        var _this = _super.call(this) || this; // base call here, field has not been set, init will be called
        _this.myMember = { value: 1 }; // field init here
        return _this;
    }
    

    您应该考虑一个解决方案,从实例外部而不是在构造函数中调用 init:

    class A {
        constructor(){
        }
        init(){}
    }
    
    class B extends A {
        private myMember = {value:1};
        constructor(){
            super();
        }
        init(){
            console.log(this.myMember.value);
        }
    }
    
    const x = new B();
    x.init();   
    

    或者你可以给你的构造函数添加一个额外的参数来指定是否调用init而不是在派生类中调用它。

    class A {
        constructor()
        constructor(doInit: boolean)
        constructor(doInit?: boolean){
            if(doInit || true)this.init();
        }
        init(){}
    }
    
    class B extends A {
        private myMember = {value:1};
        constructor()
        constructor(doInit: boolean)
        constructor(doInit?: boolean){
            super(false);
            if(doInit || true)this.init();
        }
        init(){
            console.log(this.myMember.value);
        }
    }
    
    const x = new B();
    

    或者setTimeout 的非常非常非常肮脏的解决方案,它将延迟初始化直到当前帧完成。这将让父构造函数调用完成,但是在构造函数调用和对象尚未被inited 时超时到期之间会有一个过渡时间

    class A {
        constructor(){
            setTimeout(()=> this.init(), 1);
        }
        init(){}
    }
    
    class B extends A {
        private myMember = {value:1};
        constructor(){
            super();
        }
        init(){
            console.log(this.myMember.value);
        }
    }
    
    const x = new B();
    // x is not yet inited ! but will be soon 
    

    【讨论】:

      【解决方案2】:

      因为myMember 属性是在父构造函数中访问的(init()super() 调用期间被调用),所以没有办法在子构造函数中定义它而不遇到竞争条件。

      有几种替代方法。

      init钩子

      init 被认为是一个不应在类构造函数中调用的钩子。相反,它被显式调用:

      new B();
      B.init();
      

      或者它被框架隐式调用,作为应用程序生命周期的一部分。

      静态属性

      如果一个属性应该是一个常量,它可以是静态属性。

      这是最有效的方法,因为这是静态成员的用途,但语法可能没有那么吸引人,因为如果应在子类中正确引用静态属性,则需要使用 this.constructor 而不是类名:

      class B extends A {
          static readonly myMember = { value: 1 };
      
          init() {
              console.log((this.constructor as typeof B).myMember.value);
          }
      }
      

      属性获取器/设置器

      可以使用get/set 语法在类原型上定义属性描述符。如果一个属性应该是原始常量,它可以只是一个 getter:

      class B extends A {
          get myMember() {
              return 1;
          }
      
          init() {
              console.log(this.myMember);
          }
      }
      

      如果属性不是常量或原始的,它会变得更加hacky:

      class B extends A {
          private _myMember?: { value: number };
      
          get myMember() {
              if (!('_myMember' in this)) {
                  this._myMember = { value: 1 }; 
              }
      
              return this._myMember!;
          }
          set myMember(v) {
              this._myMember = v;
          }
      
          init() {
              console.log(this.myMember.value);
          }
      }
      

      就地初始化

      一个属性可以在它首先被访问的地方被初始化。如果这种情况发生在 init 方法中,其中 this 可以在 B 类构造函数之前访问,这应该发生在那里:

      class B extends A {
          private myMember?: { value: number };
      
          init() {
              this.myMember = { value: 1 }; 
              console.log(this.myMember.value);
          }
      }
      

      异步初始化

      init 方法可能会变成异步的。初始化状态应该是可跟踪的,因此该类应该为此实现一些 API,例如基于承诺:

      class A {
          initialization = Promise.resolve();
          constructor(){
              this.init();
          }
          init(){}
      }
      
      class B extends A {
          private myMember = {value:1};
      
          init(){
              this.initialization = this.initialization.then(() => {
                  console.log(this.myMember.value);
              });
          }
      }
      
      const x = new B();
      x.initialization.then(() => {
          // class is initialized
      })
      

      对于这种特殊情况,这种方法可能被视为反模式,因为初始化例程本质上是同步的,但它可能适用于异步初始化例程。

      脱糖类

      由于 ES6 类在 super 之前对 this 的使用有限制,因此可以将子类脱糖为函数来规避此限制:

      interface B extends A {}
      interface BPrivate extends B {
          myMember: { value: number };
      }
      interface BStatic extends A {
          new(): B;
      }
      const B = <BStatic><Function>function B(this: BPrivate) {
          this.myMember = { value: 1 };
          return A.call(this); 
      }
      
      B.prototype.init = function () {
          console.log(this.myMember.value);
      }
      

      这很少是一个好的选择,因为脱糖类应该在 TypeScript 中额外键入。这也不适用于原生父类(TypeScript es6esnext 目标)。

      【讨论】:

        【解决方案3】:

        您可以采取的一种方法是为 myMember 使用 getter/setter,并在 getter 中管理默认值。这将防止未定义的问题,并允许您保持几乎完全相同的结构。像这样:

        class A {
            constructor(){
                this.init();
            }
            init(){}
        }
        
        class B extends A {
            private _myMember;
            constructor(){
                super();
            }
            init(){
                console.log(this.myMember.value);
            }
        
            get myMember() {
                return this._myMember || { value: 1 };
            }
        
            set myMember(val) {
                this._myMember = val;
            }
        }
        
        const x = new B();
        

        【讨论】:

          【解决方案4】:

          试试这个:

          class A {
              constructor() {
                  this.init();
              }
              init() { }
          }
          
          class B extends A {
              private myMember = { 'value': 1 };
              constructor() {
                  super();
              }
              init() {
                  this.myMember = { 'value': 1 };
                  console.log(this.myMember.value);
              }
          }
          
          const x = new B();
          

          【讨论】:

          • 是的,但在这种情况下,我必须重新声明 myMember,我不想这样做。
          【解决方案5】:

          超级必须是第一个命令。请记住,打字稿更像是“带有类型文档的 javascript”,而不是语言本身。

          如果您查看转译后的代码 .js,它是清晰可见的:

          class A {
              constructor() {
                  this.init();
              }
              init() {
              }
          }
          class B extends A {
              constructor() {
                  super();
                  this.myMember = { value: 1 };
              }
              init() {
                  console.log(this.myMember.value);
              }
          }
          const x = new B();
          

          【讨论】:

          • 它也是如此。 init() 将在 this.myMember 被声明之前被调用。
          • @Adam - 再次阅读答案,我已经发布了由 typescript 创建的 .js 文件。
          • 好的,没错,对不起。但这不是我的问题的解决方案:-/
          • @Adam - 没有解决办法,你不能这样用
          【解决方案6】:

          你必须在 A 类中调用 init 吗?

          这很好,但我不知道你是否有不同的要求:

          class A {
            constructor(){}
            init(){}
          }
          
          class B extends A {
            private myMember = {value:1};
            constructor(){
                super();
                this.init();
            }
            init(){
                console.log(this.myMember.value);
            }
          }
          
          const x = new B();
          

          【讨论】:

            【解决方案7】:

            像这样:

             class A
            {
                 myMember; 
                constructor() {
            
                }
            
                show() {
                    alert(this.myMember.value);
                }
            }
            
            class B extends A {
                public myMember = {value:1};
            
                constructor() {
                    super();
                }
            }
            
            const test = new B;
            test.show();
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2018-12-12
              • 2015-03-21
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2013-05-07
              • 2013-11-05
              相关资源
              最近更新 更多