TypeScript "bu" kapsam belirleme sorunu jquery geri aramasında çağrıldığında


107

TypeScript'te "this" kapsamını işlemek için en iyi yaklaşımdan emin değilim.

TypeScript'e dönüştürdüğüm kodda yaygın bir model örneği:

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

Şimdi, aramayı şu şekilde değiştirebilirim ...

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

... işe yarıyor. Ama bu biraz korkunç. Bu, kodun bazı durumlarda tümünün derlenebileceği ve iyi çalışabileceği anlamına gelir, ancak kapsamı bağlamayı unutursak kırılır.

Bunu sınıf içinde yapmanın bir yolunu istiyorum, böylece sınıfı kullanırken "bu" nun kapsamının ne olduğu konusunda endişelenmemize gerek kalmaz.

Baska öneri?

Güncelleme

İşe yarayan başka bir yaklaşım da şişman ok kullanmaktır:

class DemonstrateScopingProblems {
    private status = "blah";

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

Bu geçerli bir yaklaşım mı?


2
Bu yardımcı olabilir: youtube.com/watch?v=tvocUcbCupA
basarat

Not: Ryan cevabını TypeScript Wiki'ye kopyaladı .
Franklin Yu

TypeScript 2+ çözümü için buraya bakın .
Deilan

Yanıtlar:


166

Burada, her birinin kendi değiş tokuşu olan birkaç seçeneğiniz var. Maalesef bariz bir en iyi çözüm yoktur ve bu gerçekten uygulamaya bağlı olacaktır.

Otomatik Sınıf Bağlama
Sorunuzda gösterildiği gibi:

class DemonstrateScopingProblems {
    private status = "blah";

    public run = () => {
        alert(this.status);
    }
}
  • İyi / kötü: Bu, sınıfınızın örneği başına yöntem başına ek bir kapatma oluşturur. Bu yöntem genellikle yalnızca normal yöntem çağrılarında kullanılıyorsa, bu aşırıdır. Bununla birlikte, geri arama konumlarında çok kullanılırsa, sınıf örneğinin this, her çağrı sitesinin, çağrıldığında yeni bir kapanış oluşturması yerine bağlamı yakalaması daha etkilidir .
  • İyi: Dışarıdan arayanların thisbağlamla ilgilenmeyi unutması imkansız
  • İyi: TypeScript'te Typesafe
  • İyi: İşlevin parametreleri varsa fazladan çalışma gerekmez
  • Kötü: Türetilmiş sınıflar, bu şekilde yazılmış temel sınıf yöntemlerini arayamaz. super.
  • Kötü: Hangi yöntemlerin "önceden bağlanmış" olduğu ve sınıfınız ile tüketicileri arasında tür güvenli olmayan ek bir sözleşme oluşturmayan tam anlambilim.

Function.bind
Ayrıca gösterildiği gibi:

$(document).ready(thisTest.run.bind(thisTest));
  • İyi / kötü: İlk yönteme kıyasla ters bellek / performans değiş tokuşu
  • İyi: İşlevin parametreleri varsa fazladan çalışma gerekmez
  • Kötü: TypeScript'te, şu anda bunun tür güvenliği yoktur
  • Kötü: Yalnızca ECMAScript 5'te mevcuttur, sizin için önemliyse
  • Kötü: Örnek adını iki kez yazmanız gerekiyor


TypeScript'te şişman ok (burada açıklayıcı nedenlerle bazı sahte parametrelerle gösterilmiştir):

$(document).ready((n, m) => thisTest.run(n, m));
  • İyi / kötü: İlk yönteme kıyasla ters bellek / performans değiş tokuşu
  • İyi: TypeScript'te bu% 100 tür güvenliğine sahiptir
  • İyi: ECMAScript 3'te çalışıyor
  • İyi: Örnek adını yalnızca bir kez yazmanız gerekir
  • Kötü: Parametreleri iki kez yazmanız gerekecek
  • Kötü: Değişken parametrelerle çalışmaz

1
+1 Harika cevap Ryan, artıların ve eksilerin dağılımını seviyorum, teşekkürler!
Jonathan Moffatt

- Function.bind'ınızda, etkinliği eklemeniz gereken her zaman yeni bir kapanış yaratırsınız.
131

1
Şişman ok az önce başardı !! : D: D = () => Çok teşekkür ederim! : D
Christopher Stok

@ ryan-cavanaugh, nesnenin ne zaman serbest kalacağı açısından iyi ve kötü ne olacak? 30 dakikadan uzun süredir aktif olan bir SPA örneğinde olduğu gibi, yukarıdakilerden hangisi JS çöp toplayıcıları için en iyisidir?
abbaf33f

Sınıf örneği serbest bırakıldığında bunların tümü serbest bırakılabilir. Olay işleyicisinin ömrü daha kısaysa, son ikisi daha erken serbest bırakılabilir. Genel olarak, ölçülebilir bir fark olmayacağını söyleyebilirim.
Ryan Cavanaugh

16

Bazı başlangıç ​​kurulumları gerektiren ancak yenilmez hafif, kelimenin tam anlamıyla tek kelimelik sözdizimi ile karşılığını veren başka bir çözüm, alıcılar aracılığıyla JIT bağlama yöntemlerini kullanmak için Yöntem Dekoratörlerini kullanmaktır .

Bu fikrin bir uygulamasını sergilemek için GitHub'da bir depo oluşturdum (yorumlar dahil 40 kod satırıyla bir yanıta sığdırmak biraz uzun) , şu kadar basitçe kullanacaksınız:

class DemonstrateScopingProblems {
    private status = "blah";

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

Henüz hiçbir yerde bunun bahsettiğini görmedim, ama kusursuz çalışıyor. Ayrıca, bu yaklaşımın kayda değer bir dezavantajı yoktur: bu dekoratörün uygulanması - çalışma zamanı tip güvenliği için bazı tip kontrolleri de dahil olmak üzere - önemsiz ve basittir ve ilk yöntem çağrısından sonra esasen sıfır ek yük ile birlikte gelir.

Temel kısım, ilk çağrıdan hemen önce yürütülen sınıf prototipinde aşağıdaki alıcıyı tanımlamaktır :

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];
}

Tam kaynak


Fikir ayrıca, bunu bir sınıf dekoratöründe yaparak, yöntemleri yineleyerek ve her biri için bir geçişte yukarıdaki özellik tanımlayıcısını tanımlayarak bir adım daha ileri götürülebilir.


tam da ihtiyacım olan şey!
Marcel van der Drift

14

Büyüleyici.
Ok fonksiyonları (ok fonksiyonları% 30 daha yavaştır) veya alıcılar aracılığıyla JIT metotları gerektirmeyen bariz basit bir çözüm var.
Bu çözüm, yapıcıdaki bu bağlamı bağlamaktır.

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


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

Sınıfın yapıcısındaki tüm işlevleri otomatik olarak bağlamak için bir autobind yöntemi yazabilirsiniz:

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

Not Eğer üye işleviyle aynı sınıfa AutoBind-fonksiyonu koymazsan o, bu sadece autoBind(this);değilthis.autoBind(this);

Ayrıca, yukarıdaki autoBind işlevi, ilkeyi göstermek için basitleştirilmiştir.
Bunun güvenilir bir şekilde çalışmasını istiyorsanız, işlevin bir özelliğin alıcı / ayarlayıcısı olup olmadığını da test etmeniz gerekir, çünkü aksi takdirde - boom - sınıfınız özellikler içeriyorsa, yani.

Bunun gibi:

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)" değil "autoBind (this)" kullanmak zorunda kaldım
JohnOpincar

@JohnOpincar: evet, this.autoBind (this) autobind'in ayrı bir dışa aktarma olarak değil, sınıf içinde olduğunu varsayar.
Stefan Steiger

Şimdi anlıyorum. Yöntemi aynı sınıfa koyarsınız. Onu bir "yardımcı program" modülüne koydum.
JohnOpincar

2

Kodunuzda, son satırı aşağıdaki gibi değiştirmeyi denediniz mi?

$(document).ready(() => thisTest.run());
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.