web-dev-qa-db-ja.com

jqueryコールバックで呼び出された場合のTypeScript「this」スコープの問題

TypeScriptで "this"のスコープを処理するための最適なアプローチがわかりません。

TypeScriptに変換するコードの一般的なパターンの例を次に示します。

class DemonstrateScopingProblems {
    private status = "blah";
    public run() {
        alert(this.status);
    }
}

var thisTest = new DemonstrateScopingProblems();
// works as expected, displays "blah":
thisTest.run(); 
// doesn't work; this is scoped to be the document so this.status is undefined:
$(document).ready(thisTest.run); 

これで、呼び出しを...

$(document).ready(thisTest.run.bind(thisTest));

...これは動作します。しかし、それはちょっと恐ろしいです。状況によってはコードがすべてコンパイルされて正常に動作できることを意味しますが、スコープのバインドを忘れると破損します。

クラス内でそれを行う方法が欲しいので、クラスを使用するときに「this」のスコープを心配する必要はありません。

助言がありますか?

更新

動作するもう1つのアプローチは、太い矢印を使用することです。

class DemonstrateScopingProblems {
    private status = "blah";

    public run = () => {
        alert(this.status);
    }
}

それは有効なアプローチですか?

104

ここにはいくつかのオプションがあり、それぞれにトレードオフがあります。残念ながら、明らかな最善の解決策はなく、アプリケーションに依存します。

自動クラスバインディング
質問のとおり:

class DemonstrateScopingProblems {
    private status = "blah";

    public run = () => {
        alert(this.status);
    }
}
  • 良い/悪い:これにより、クラスのインスタンスごとにメソッドごとに追加のクロージャーが作成されます。通常、このメソッドが通常のメソッド呼び出しでのみ使用される場合、これは過剰です。ただし、コールバック位置で多く使用される場合、各呼び出しサイトが呼び出し時に新しいクロージャーを作成する代わりに、クラスインスタンスがthisコンテキストをキャプチャする方が効率的です。
  • 良い:外部呼び出し元がthisコンテキストの処理を忘れることは不可能
  • 良い:TypeScriptでタイプセーフ
  • 良い:関数にパラメーターがある場合、余分な作業はありません
  • 悪い:派生クラスは、super.を使用してこのように記述された基本クラスメソッドを呼び出すことができません
  • 悪い:どのメソッドが「事前バインド」され、どのメソッドがクラスとそのコンシューマーの間で追加の非タイプセーフコントラクトを作成しないかの正確なセマンティクス。

Function.bind
また、次のとおりです。

$(document).ready(thisTest.run.bind(thisTest));
  • 良い/悪い:最初の方法と比較してメモリ/パフォーマンスのトレードオフが反対
  • 良い:関数にパラメーターがある場合、余分な作業はありません
  • 悪い:TypeScriptでは、現在これには型の安全性がありません
  • 悪い:ECMAScript 5でのみ利用可能
  • 悪い:インスタンス名を2回入力する必要があります

太い矢印
TypeScriptで(説明のためにいくつかのダミーパラメーターを使用してここに表示)

$(document).ready((n, m) => thisTest.run(n, m));
  • 良い/悪い:最初の方法と比較してメモリ/パフォーマンスのトレードオフが反対
  • 良い:TypeScriptでは、これは100%の型安全性を持っています
  • 良い:ECMAScript 3で動作します
  • 良い:インスタンス名を1回入力するだけです
  • 悪い:パラメーターを2回入力する必要があります
  • 悪い:可変長パラメーターでは機能しません
159
Ryan Cavanaugh

初期セットアップを必要とするが、文字通り1ワードの文字通りシンタックスに優れた別のソリューションは、ゲッターを介してJITバインドメソッドに Method Decorators を使用します。

GitHubのレポ を作成して、このアイデアの実装を紹介します(コメントを含む40行のコードで回答に合わせるには少し時間がかかります)、次のように簡単に使用できます。

class DemonstrateScopingProblems {
    private status = "blah";

    @bound public run() {
        alert(this.status);
    }
}

これについてはまだ言及していませんが、問題なく動作します。また、このアプローチには顕著なマイナス面はありません:このデコレータの実装-実行時の型安全性のための型チェックを含む-は簡単で簡単であり、初期メソッド呼び出し。

重要な部分は、クラスプロトタイプで次のゲッターを定義することです。これは、最初の呼び出しであるimmediately beforeで実行されます。

get: function () {
    // Create bound override on object instance. This will hide the original method on the prototype, and instead yield a bound version from the
    // instance itself. The original method will no longer be accessible. Inside a getter, 'this' will refer to the instance.
    var instance = this;

    Object.defineProperty(instance, propKey.toString(), {
        value: function () {
            // This is effectively a lightweight bind() that skips many (here unnecessary) checks found in native implementations.
            return originalMethod.apply(instance, arguments);
        }
    });

    // The first invocation (per instance) will return the bound method from here. Subsequent calls will never reach this point, due to the way
    // JavaScript runtimes look up properties on objects; the bound method, defined on the instance, will effectively hide it.
    return instance[propKey];
}

完全なソース


アイデアをさらに一歩進めて、代わりにクラスデコレータでこれを実行し、メソッドを繰り返し処理し、1つのパスで各メソッドの上記のプロパティ記述子を定義します。

14
John Weisz

ネクロマンシング。
矢印関数(矢印関数は30%遅くなります)やゲッターを介したJITメソッドを必要としない明らかな単純なソリューションがあります。
その解決策は、コンストラクターでthis-contextをバインドすることです。

class DemonstrateScopingProblems 
{
    constructor()
    {
        this.run = this.run.bind(this);
    }


    private status = "blah";
    public run() {
        alert(this.status);
    }
}

このメソッドを使用して、コンストラクター内のクラス内のすべての関数を自動的にバインドできます。

class DemonstrateScopingProblems 
{

    constructor()
    { 
        this.autoBind(this);
    }
    [...]
}


export function autoBind(self: any)
{
    for (const key of Object.getOwnPropertyNames(self.constructor.prototype))
    {
        const val = self[key];

        if (key !== 'constructor' && typeof val === 'function')
        {
            // console.log(key);
            self[key] = val.bind(self);
        } // End if (key !== 'constructor' && typeof val === 'function') 

    } // Next key 

    return self;
} // End Function autoBind
14
Stefan Steiger

あなたのコードでは、最後の行を次のように変更しようとしましたか?

$(document).ready(() => thisTest.run());
2
Albino Cordeiro