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


107

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));

...これは機能します。しかし、それはちょっと恐ろしいことです。つまり、状況によってはコードをすべてコンパイルして正常に機能させることができますが、スコープのバインドを忘れると、コードが壊れてしまいます。

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

助言がありますか?

更新

機能する別のアプローチは太い矢印を使用することです:

class DemonstrateScopingProblems {
    private status = "blah";

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

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


2
これは役に立ちます:youtube.com/watch
v

注:ライアンは彼の回答をTypeScript Wikiにコピーしました。
フランクリンYu

TypeScript 2+ソリューションについては、こちらご覧ください。
Deilan '19

回答:


166

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

自動クラスバインディング
あなたの質問に示されているように:

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回入力する必要があります
  • 悪い例:可変パラメータでは動作しません

1
+1すばらしい回答ライアン、賛否両論の内訳が大好きです。ありがとうございます。
ジョナサンモファット

-Function.bindで、イベントをアタッチする必要があるたびに新しいクロージャーを作成します。
131

1
太った矢はちょうどそれをやった!:D:D =()=>ありがとうございます!:D
Christopher Stock

@ ryan-cavanaughオブジェクトがいつ解放されるかという点で、良い点と悪い点はどうですか?30分以上アクティブなSPAの例のように、JSガベージコレクターが処理するのに最適なのは上記のうちどれですか。
abbaf33f 2016

クラスインスタンスが解放可能である場合、これらすべてが解放可能です。後者の2つは、イベントハンドラーの有効期間が短い場合、より早く解放されます。一般的には、測定可能な違いはないと思います。
Ryan Cavanaugh

16

いくつかの初期設定を必要とするが、その無敵で軽い、文字通り1語の構文で報われる別のソリューションは、メソッドデコレータを使用することです。をて、ゲッターを通じてメソッドをJITバインドすることです。

このアイデアの実装を紹介するためにGitHubリポジトリを作成しました(コメントを含む40行のコードを含む回答に収めるには少し時間がかかります)

class DemonstrateScopingProblems {
    private status = "blah";

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

これについてはまだどこにも触れていませんが、問題なく動作します。また、このアプローチには特筆すべき欠点はありません。このデコレーターの実装(ランタイムの型安全性の型チェックを含む)は簡単で簡単で、最初のメソッド呼び出し後のオーバーヘッドは基本的にゼロです。

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

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回のパスで定義します。


ちょうど私が必要なもの!
Marcel van der Drift

14

ネクロマンシング。
アロー関数(アロー関数は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)
{
    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

autobind-functionをメンバー関数と同じクラスに入れない場合autoBind(this);は、this.autoBind(this);

また、上記のautoBind関数は、原理を示すために簡略化されています。
これを確実に機能させたい場合は、関数がプロパティのゲッター/セッターであるかどうかもテストする必要があります。それ以外の場合-ブーム-クラスにプロパティが含まれている場合はそうです。

このような:

export function autoBind(self)
{
    for (const key of Object.getOwnPropertyNames(self.constructor.prototype))
    {

        if (key !== 'constructor')
        {
            // console.log(key);

            let desc = Object.getOwnPropertyDescriptor(self.constructor.prototype, key);

            if (desc != null)
            {
                let g = desc.get != null;
                let s = desc.set != null;

                if (g || s)
                {
                    if (g)
                        desc.get = desc.get.bind(self);

                    if (s)
                        desc.set = desc.set.bind(self);

                    Object.defineProperty(self.constructor.prototype, key, desc);
                    continue; // if it's a property, it can't be a function 
                } // End if (g || s) 

            } // End if (desc != null) 

            if (typeof (self[key]) === 'function')
            {
                let val = self[key];
                self[key] = val.bind(self);
            } // End if (typeof (self[key]) === 'function') 

        } // End if (key !== 'constructor') 

    } // Next key 

    return self;
} // End Function autoBind

「this.autoBind(this)」ではなく「autoBind(this)」を使用する必要があった
JohnOpincar '30年

@JohnOpincar:うん、this.autoBind(this)はautobindが個別のエクスポートとしてではなく、クラス内にあると想定しています。
Stefan Steiger

今、私は分かる。メソッドを同じクラスに配置します。「ユーティリティ」モジュールに入れました。
JohnOpincar、

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.