クラス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コードがinit
を作成する前にmyMember
メソッドを呼び出すことは私には明らかですが、それを機能させるには、いくつかのプラクティス/パターンが必要です。
これが、一部の言語(咳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
の非常に非常にダーティなソリューション。これにより、親コンストラクターの呼び出しは完了しますが、コンストラクターの呼び出しと、オブジェクトがinit
edされていないときにタイムアウトになるまでの間に暫定的になります。
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
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
構文を使用してクラスプロトタイプで定義できます。プロパティがプリミティブ定数であると想定されている場合、それは単なるゲッターである可能性があります。
class B extends A {
get myMember() {
return 1;
}
init() {
console.log(this.myMember);
}
}
プロパティが定数またはプリミティブでない場合、よりハッキングされます。
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クラスはthis
より前にsuper
の使用に制限があるため、子クラスをこの制限を回避するために関数にデガッシュできます。
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 es6
およびesnext
ターゲット)でも機能しません。
あなたが取ることができる1つのアプローチは、myMemberにゲッター/セッターを使用し、ゲッターでデフォルト値を管理することです。これにより、未定義の問題が回避され、ほぼ同じ構造を維持できます。このような:
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();
スーパーは最初のコマンドでなければなりません。 TypeScriptは、それ自体が言語というよりも、「型のドキュメントを含む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();
クラス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();
これを試して:
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();
このような :
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();