TypeScript masalah cakupan "ini" ketika dipanggil dalam jquery callback

107

Saya tidak yakin pendekatan terbaik untuk menangani pelingkupan "ini" di TypeScript.

Berikut adalah contoh pola umum dalam kode yang saya ubah ke 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); 

Sekarang, saya dapat mengubah panggilan ke ...

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

... yang berhasil. Tapi itu mengerikan. Ini berarti bahwa kode semua dapat dikompilasi dan berfungsi dengan baik dalam beberapa keadaan, tetapi jika kita lupa untuk mengikat cakupan, itu akan rusak.

Saya ingin cara melakukannya di dalam kelas, sehingga saat menggunakan kelas kita tidak perlu khawatir tentang cakupan "ini".

Ada saran?

Memperbarui

Pendekatan lain yang berhasil adalah menggunakan panah gemuk:

class DemonstrateScopingProblems {
    private status = "blah";

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

Apakah itu pendekatan yang valid?

Jonathan Moffatt
sumber
2
Ini akan membantu: youtube.com/watch?v=tvocUcbCupA
basarat
Catatan: Ryan menyalin jawabannya ke TypeScript Wiki .
Franklin Yu
Lihat di sini untuk solusi TypeScript 2+.
Deilan

Jawaban:

166

Anda memiliki beberapa opsi di sini, masing-masing dengan keuntungannya sendiri. Sayangnya tidak ada solusi terbaik yang jelas dan itu akan sangat bergantung pada aplikasi.

Pengikatan Kelas Otomatis
Seperti yang ditunjukkan dalam pertanyaan Anda:

class DemonstrateScopingProblems {
    private status = "blah";

    public run = () => {
        alert(this.status);
    }
}
  • Baik / buruk: Tindakan ini membuat penutupan tambahan per metode per instance kelas Anda. Jika metode ini biasanya hanya digunakan dalam pemanggilan metode biasa, ini berlebihan. Namun, jika itu banyak digunakan dalam posisi callback, itu lebih efisien untuk instance kelas untuk menangkap thiskonteks daripada setiap situs panggilan membuat penutupan baru saat dipanggil.
  • Bagus: Tidak mungkin penelepon eksternal lupa menangani thiskonteks
  • Bagus: Ketikan aman di TypeScript
  • Bagus: Tidak ada pekerjaan tambahan jika fungsi memiliki parameter
  • Buruk: Kelas turunan tidak dapat memanggil metode kelas dasar yang ditulis dengan cara ini super.
  • Buruk: Semantik yang tepat dari metode mana yang "terikat sebelumnya" dan yang tidak membuat kontrak tidak aman tambahan antara kelas Anda dan konsumennya.

Function.bind
Juga seperti yang ditunjukkan:

$(document).ready(thisTest.run.bind(thisTest));
  • Baik / buruk: Pertukaran memori / kinerja berlawanan dengan metode pertama
  • Bagus: Tidak ada pekerjaan tambahan jika fungsi memiliki parameter
  • Buruk: Di TypeScript, saat ini tidak memiliki keamanan jenis
  • Buruk: Hanya tersedia di ECMAScript 5, jika itu penting bagi Anda
  • Buruk: Anda harus mengetik nama instance dua kali

Panah gemuk
Dalam TypeScript (ditampilkan di sini dengan beberapa parameter tiruan untuk alasan penjelasan):

$(document).ready((n, m) => thisTest.run(n, m));
  • Baik / buruk: Pertukaran memori / kinerja berlawanan dengan metode pertama
  • Bagus: Di TypeScript, ini memiliki keamanan tipe 100%
  • Baik: Bekerja di ECMAScript 3
  • Bagus: Anda hanya perlu mengetikkan nama instance satu kali
  • Buruk: Anda harus mengetikkan parameter dua kali
  • Buruk: Tidak berfungsi dengan parameter variadic
Ryan Cavanaugh
sumber
1
+1 Jawaban hebat Ryan, suka uraian pro dan kontra, terima kasih!
Jonathan Moffatt
- Di Function.bind Anda, Anda membuat penutupan baru setiap kali Anda perlu melampirkan acara.
131
1
Panah gemuk baru saja melakukannya !! : D: D = () => Terima kasih banyak! : D
Christopher Stock
@ ryan-cavanaugh bagaimana dengan yang baik dan yang buruk dalam hal kapan benda tersebut akan dibebaskan? Seperti pada contoh SPA yang aktif selama> 30 menit, manakah di atas yang paling baik untuk ditangani oleh pengumpul sampah JS?
abbaf33f
Semua ini akan dapat digratiskan saat instance kelas dapat digratiskan. Dua yang terakhir akan dapat dibebaskan lebih awal jika masa pakai pengendali acara lebih pendek. Secara umum, saya akan mengatakan tidak akan ada perbedaan yang terukur.
Ryan Cavanaugh
16

Solusi lain yang membutuhkan beberapa pengaturan awal tetapi terbayar dengan cahaya yang tak terkalahkan, secara harfiah sintaks satu kata menggunakan Metode Dekorator untuk mengikat JIT metode melalui getter.

Saya telah membuat repo di GitHub untuk memamerkan implementasi ide ini (agak panjang untuk memasukkan jawaban dengan 40 baris kode, termasuk komentar) , yang akan Anda gunakan sesederhana:

class DemonstrateScopingProblems {
    private status = "blah";

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

Saya belum melihat ini disebutkan di mana pun, tetapi berfungsi dengan sempurna. Selain itu, tidak ada kerugian penting untuk pendekatan ini: penerapan dekorator ini - termasuk beberapa pemeriksaan jenis untuk keamanan jenis runtime - adalah hal yang sepele dan mudah, dan pada dasarnya dilengkapi dengan overhead nol setelah pemanggilan metode awal.

Bagian penting adalah mendefinisikan pengambil berikut pada prototipe kelas, yang dijalankan segera sebelum panggilan pertama:

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

Sumber lengkap


Idenya juga dapat dibawa selangkah lebih maju, dengan melakukan ini di dekorator kelas, mengiterasi metode dan menentukan deskriptor properti di atas untuk masing-masing metode dalam satu proses.

John Weisz
sumber
hanya apa yang saya butuhkan!
Marcel van der Drift
14

Necromancing.
Ada solusi sederhana yang jelas yang tidak memerlukan fungsi panah (fungsi panah 30% lebih lambat), atau metode JIT melalui getter.
Solusi itu adalah mengikat konteks-ini di konstruktor.

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


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

Anda dapat menulis metode autobind untuk mengikat semua fungsi secara otomatis dalam konstruktor kelas:

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

Perhatikan bahwa jika Anda tidak menempatkan fungsi autobind ke dalam kelas yang sama sebagai fungsi anggota, itu adil autoBind(this);dan tidakthis.autoBind(this);

Dan juga, fungsi autoBind di atas dibodohi, untuk menunjukkan prinsip.
Jika Anda ingin ini berfungsi dengan andal, Anda perlu menguji apakah fungsinya adalah pengambil / penyetel properti juga, karena sebaliknya - boom - jika kelas Anda berisi properti, begitulah.

Seperti ini:

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
Stefan Steiger
sumber
Saya harus menggunakan "autoBind (this)" bukan "this.autoBind (this)"
JohnOpincar
@JohnOpincar: ya, this.autoBind (this) mengasumsikan autobind ada di dalam kelas, bukan sebagai ekspor terpisah.
Stefan Steiger
Saya mengerti sekarang. Anda menempatkan metode pada kelas yang sama. Saya memasukkannya ke dalam modul "utilitas".
JohnOpincar
2

Dalam kode Anda, sudahkah Anda mencoba mengubah baris terakhir sebagai berikut?

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