typescript - Declarații de funcții sau expresii pentru metodele clasei?


Un coleg de-al meu și cu mine am avut această discuție de mai multe ori. Există două moduri de a defini metodele unei clase. Primul mod este cu o declarație de funcție:

class Action {
    public execute(): void {
        this.doSomething();
    }
    ...
}

Declarațiile funcțiilor tind să fie mai ușor de citit. Doar un singur obiect de funcție este utilizat pentru fiecare instanță de Action, astfel că sunt și mai eficiente din punct de vedere al memoriei.

Al doilea este cu o expresie de funcție:

class Action {
    public execute: () => void = () => {
        this.doSomething();
    };
    ...
}

Expresiile funcționale necesită mai mult tipaj (în special cu definiții de tip), sunt mai greu de citit și generează un obiect funcțional nou pentru fiecare instanță de Action. Și dacă generați o mulțime de obiecte, asta este rău.

Cu toate acestea, expresiile funcție au un mic avantaj: ele păstrează contextul lui this (adică instanța Action) indiferent cine le apelează:

var instance = new Action();
setTimeout(instance.execute);

O metodă declarată ca expresie funcțională funcționează conform așteptărilor în acest caz. Declarațiile funcțiilor eșuează lamentabil, dar pot fi ușor corectate făcând asta în schimb:

var instance = new Action();

setTimeout(() => instance.execute());
// or
setTimeout(instance.execute.bind(instance));

Deci, unul este considerat o practică mai bună decât celălalt, sau este pur și simplu o chestiune de situație/preferență?

Răspuns #1

În opinia mea, funcțiile săgeată ar trebui utilizate ca metode de clasă doar atunci când știi sigur că funcția ar putea fi apelată cu un context diferit pentru this (dacă este transmisă, de exemplu, ca handler de eveniment) și preferi să eviți utilizarea Function.prototype.bind.

Există mai multe motive pentru asta, inclusiv, așa cum ai scris, pentru lizibilitatea codului, dar motivul principal este pentru moștenire. Dacă folosești o funcție săgeată, atunci pur și simplu atribui o funcție instanței ca membru, dar funcția nu va fi adăugată la prototip:

// ts
class A {
    fn1() {}

    fn2 = () => {}
}

// js
var A = (function () {
    function A() {
        this.fn2 = function () { };
    }
    A.prototype.fn1 = function () { };
    return A;
}());

Deci ce se întâmplă dacă vrei să extinzi această clasă și să suprascrii metoda fn2?
Pentru că este o proprietate și nu face parte din prototip, va trebui să faci ceva de genul:

class B extends A {
    private oldFn2 = this.fn2;

    fn2 = () => {
        this.fn2();
    }
}

Ce arată pur și simplu teribil în comparație cu:

class A {
    fn1() {}

    fn2() {}
}

class B extends A {
    fn2() {
        super.fn2();
    }
}

Există câteva motive pentru a prefera utilizarea metodei bind în locul unei funcții anonime. Eu o consider mai implicită, deoarece este exact aceeași funcție, dar legată de un this specific. Pe de altă parte, într-o funcție anonimă, poți adăuga mai mult cod în afară de apelarea funcției propriu-zise.

Alt lucru este că funcția bind îți permite nu doar să legi care obiect va fi considerat ca this, ci și să legi parametrii:

function fn(one, two, three) {}
fn.bind(null, 1, 2)(3);
fn(1, 2, 3);

Cele două apeluri ale fn aici sunt aceleași.
Poți face asta cu funcții anonime, dar nu întotdeauna:

var a = ["zero", "one", "two", "three", "four", "five"];
function fn(value, index) {
    console.log(value, index);
}

// works
a.forEach((item, index) => {
    setTimeout(() => {
        fn(item, index);
    }, 45);
});

// works
for (let i = 0; i < a.length; i++) {
    setTimeout(() => {
        fn(a[i], i);
    }, 45);
}

// doesn't work as i is undefined when the function is invoked
for (var i = 0; i < a.length; i++) {
    setTimeout(() => {
        fn(a[i], i);
    }, 45);
}

// works because the value of i and the value of a[i] are bound
for (var i = 0; i < a.length; i++) {
    setTimeout(fn.bind(null, a[i], i), 45);
}