Funkcje TypeScript dynamicznie lub programowo
Tworzenie łańcuchów funkcji TypeScript, ale chcę je programowo łączyć.
Przykładowa klasa : chain.ts
class MyChain {
value: number = 0;
constructor() {
this.value = 0;
}
sum(args: number[]) {
this.value = args.reduce((s, c) => s + c, 0);
return this;
}
add(v: number) {
this.value = this.value + v;
return this;
}
subtract(v: number) {
this.value = this.value - v;
return this;
}
}
const mc = new MyChain();
console.log(mc.sum([1, 2, 3, 4]).subtract(5).value);
Widzę numer 5na konsoli.
Wciąż jestem całkiem nowy w JavaScript i TypeScript, więc doszedłem do wniosku, że funkcja w tej klasie jest w rzeczywistości elementem tablicy instancji tej klasy. Dlatego mogę to zrobić:
console.log(mc["sum"]([1, 2, 3, 4]).value);
To rzeczywiście zwraca liczbę 10.
Teraz jestem zdezorientowany, jak połączyć to programowo. Na przykład (to oczywiście nie jest to, co i tak bym chciał robić i pokazuje mój beznadziejny brak zrozumienia JavaScript:
console.log(mc["sum"]([1, 2, 3, 4]).mc["subtract"](5).value);
Błąd:
Właściwość „mc” nie istnieje w typie „MyChain” .ts (2339)
Okej, szczerze mówiąc, intuicyjnie wiedziałem, że to nie zadziała. Jednak myśląc o tym, w jaki sposób miałbym uzyskać dostęp do elementów wielowymiarowej tablicy w jakimkolwiek rozsądnym języku?
console.log(mc["sum"]([1, 2, 3, 4])["subtract"](5).value);
Bingo. To załatwia sprawę. Ale to naprawdę nie jest rozwiązanie, którego potrzebuję. Potrzebuję czegoś takiego:
interface IChainObject {
action: string;
operand: number | number[];
}
const chainObj: IChainObject[] = [
{ action: "sum", operand: [1, 2, 3, 4] },
{ action: "subtract", operand: 5 },
];
Na początek chciałbym spróbować tego:
console.log(mc[chainObj[0].action](chainObj[0].operand).value);
I w konsekwencji wygenerowanie mechanizmu, który ostatecznie zbudowałby coś takiego:
console.log(
mc[chainObj[0].action](chainObj[0].operand)[chainObj[1].action](
chainObj[1].operand
).value
);
Dlatego wydaje mi się, że to, czego chcę, to jakiś sposób na wygenerowanie tego:
[chainObj[0].action](chainObj[0].operand)[chainObj[1].action](chainObj[1].operand)
z tego, z moim obiektem łańcucha mającym jeden lub wiele zestawów obiektów akcji / operandów:
const chainObj: IChainObject[] = [
{ action: "sum", operand: [1, 2, 3, 4] },
{ action: "subtract", operand: 5 },
];
W tym miejscu mój mózg mniej więcej się wyłącza. Myślę, że muszę wygenerować łańcuch wartości ciągów, ale będą to po prostu ciągi i tak naprawdę nie będą działać jako indeksy tablicy w funkcji, jak chcę.
Dlaczego chcę to zrobić? Ostatecznie chcę zbudować złożony obiekt schematu Yup z obiektu JSON. Znalazłem ten doskonały post , ale moim głównym problemem jest to, że nie rozumiem, jak działa ten kod.
W tym momencie jestem w stanie przeanalizować sposób, w jaki Vijay był w stanie rozwiązać swój problem i w pewien sposób go naśladować. Oto działający kod dla mojego przykładu:
const mc = new MyChain();
interface IChainObject {
action: string;
operand: number | number[];
}
const chainObj: IChainObject[] = [
{ action: "sum", operand: [1, 2, 3, 4, 5] },
{ action: "subtract", operand: 5 },
];
let myChain = {};
chainObj.forEach((o) => {
myChain = mc[o.action](o.operand);
});
console.log("myChain is", myChain["value"]);
Prowadzi do: myChain is 10
Prawdopodobnie zadajesz sobie pytanie: „W czym problem, Dan?”. Wydaje się, że masz teraz rozwiązanie w ręku. Tak, chyba tak, ale nie rozumiem. Po prostu kopiuję i wklejam kod, nieznacznie go rozumiem i wprowadzam zmiany, które sprawiają, że działa.
Moim podstawowym problemem jest to, że nie rozumiem, jak działa ta linia kodu: myChain = mc[o.action](o.operand);
Rozumiem, że wywołuje funkcję w oparciu o akcję i dostarcza dane do funkcji za pośrednictwem operandu. Jestem małpą kodu kopiowania i wklejania. Chcę być kimś więcej niż małpą. Może pawian lub nawet małpa. Dlatego chcę zrozumieć, co zrobiłem. To, co nie ma dla mnie sensu, to sposób, w jaki to łączy.
Pomyślałem, że może sekret tkwi w funkcji forEach , ale to nie jest to. Oto prosty test:
let p = 0;
const x = [1, 2, 3, 4];
x.forEach((y) => {
p = y;
});
console.log("p is", p); p is 4
Jaka jest tajemnicza magia JavaScript, która dzieje się pod maską, która sprawia, że myChain = mc[o.action](o.operand);kod faktycznie łączy moje funkcje, zamiast po prostu działać jedna, a praca druga. Po prostu tego nie widzę.
Odpowiedzi
Zacznijmy od pierwszego nieporozumienia, jakie mogę znaleźć:
Wciąż jestem całkiem nowy w JavaScript i TypeScript, więc doszedłem do wniosku, że funkcja w tej klasie jest w rzeczywistości elementem tablicy instancji tej klasy.
Nie o to chodzi. Nawiasy kwadratowe w JavaScript są używane do wszystkich wyszukiwań właściwości, a nie tylko do indeksowania tablic. x.foojest właściwie równoważne x["foo"]i ta sama składnia działa dla tablic, ponieważ tablice są tylko obiektami . Klasy w Javascript to po prostu obiekty, które mają własność prototypu , która sama jest obiektem. Zawiera listę atrybutów domyślnych, a jeśli utworzysz instancję klasy i wyszukasz właściwość, której nie ma w obiekcie, wyszuka ją w prototypie. Patrząc na kod:
mc["sum"]([1, 2, 3])
Szuka właściwości „sum” w mci nie może znaleźć żadnej, ponieważ jej nie zdefiniowałeś, więc szuka w prototypeof MyChaini znajduje mcmetodę. Tak więc mc["sum"]jest summetoda mc. Teraz ten kod:
console.log(mc["sum"]([1, 2, 3, 4]).mc["subtract"](5).value);
nie działa i nie bez powodu wygląda to bardzo źle. mc["sum"]([1, 2, 3, 4])zwraca mc, więc dlaczego miałbyś mieć dostęp do mcwłaściwości (nie żeby w mcogóle istniała)? Dlatego twój drugi przykład, ten, który wywołuje subtractbezpośrednio, działa:
console.log(mc["sum"]([1, 2, 3, 4])["subtract"](5).value);
Teraz spójrzmy na działający kod:
const mc = new MyChain();
interface IChainObject {
action: string;
operand: number | number[];
}
const chainObj: IChainObject[] = [
{ action: "sum", operand: [1, 2, 3, 4, 5] },
{ action: "subtract", operand: 5 },
];
let myChain = {};
chainObj.forEach((o) => {
myChain = mc[o.action](o.operand);
});
console.log("myChain is", myChain["value"]);
Właściwie nie potrzebujesz dużo tego kodu. Można to uprościć do:
const mc = new MyChain();
interface IChainObject {
action: keyof MyChain;
operand: number | number[];
}
const chainObj: IChainObject[] = [
{ action: "sum", operand: [1, 2, 3, 4, 5] },
{ action: "subtract", operand: 5 },
];
chainObj.forEach((o) => {
// bypass typescript type checking with cast
(mc[o.action] as Function)(o.operand);
});
console.log("myChain is", mc.value);
Zasadniczo forEachpętle przechodzą przez elementy chainObjw kolejności. Wartość elementu jest przechowywana w zmiennej o. mc[o.action]pobiera nazwę metody przechowywaną w o.actioni uzyskuje do niej dostęp za pomocą nawiasów kwadratowych. To po prostu sprawdzenie metody. Następnie metoda jest wywoływana with (o.operand)(w JavaScript funkcje to tylko wartości i możesz wywołać dowolną wartość, taką jak funkcja, ale jeśli nie jest to funkcja, spowoduje to błąd). mcnastępnie modyfikuje się i przechodzisz do następnej pętli. Jeśli wstawimy debuggerinstrukcję do funkcji, a następnie przerwiemy w pierwszej pętli, możemy sprawdzić zmienne:
Jak widać, wartość zaczyna się od 0, o.actionjest „sumą” i mc[o.action]jest metodą sumowania. Następnie możemy wywołać metodę sum with o.operand, która dodaje elementy i ustawia wartość na 15. Następnie w drugiej pętli:
mc[o.action]jest metodą odejmowania i nazywamy ją metodą, o.operandktóra wynosi 5, obniżając wartość do 10.
Większość rzeczy w Javascript classesjest w zasadzie tylko objects. 1
Oznacza to, że dostęp do atrybutów lub w tym przypadku do funkcji można uzyskać za pomocą notacji z kropkami lub notacji w nawiasach .
Spójrzmy na przykład, który może pomóc w wyjaśnieniu:
class MyClass {
myFunction(x) {
console.log(x);
}
}
const x = new MyClass();
// attribute accessed via the dot notation
x.myFunction("Hello World!");
// attribute accessed via the bracket notation and a string
x['myFunction']("Hello World, again!");
// attribute accessed via a variable that is a string
const functionName = 'myFunction';
x[functionName]("Well uh, Hello World again?");
// attribute accessed via a variable that is a string, and passing in an argument
const argument = "This is " + "an argument";
x[functionName](argument);
Aby dalej zilustrować tę kwestię:
class MyClass {
myFunction(x) {
console.log(x);
}
}
const x = new MyClass();
console.log(x.myFunction) // returns a function
console.log(x["myFunction"]) // returns a function
// executing the function
x.myFunction("Method One");
x["myFunction"]("Method Two")
Widzimy, że zwracaną funkcję można wywołać.
Wróćmy więc do twojego przykładu
chainObj.forEach((o) => {
myChain = mc[o.action](o.operand);
});
o.actionto nazwa funkcjio.operandjest argumentem Dlatego z grubsza przekłada się na:
chainObj.forEach((o) => {
myChain = mc[functionName](arugment);
});
tak jak nasze poprzednie przykłady.
1 „klasy to po prostu obiekty”
Jest tak wiele elementów tego; Skoncentruję się tylko na tym,forEach() " jaki jest sekret, który sprawia, że kod działa? "
Sekret polega na tym, że instancje MyChainmają właściwość o nazwie, valuektóra jest aktualizowana po wywołaniu każdej metody. Kod z forEach()jest naprawdę nie łańcuchowego połączenia ze sobą ; po prostu operuje na oryginalnej MyChainzmiennej o nazwie za mckażdym razem.
Ponieważ wszystkie metody MyChaintej aktualizacji this.valuerównież zwracają this, nie ma znaczenia, czy naprawdę łańcuchowo wywołujesz (operujesz na wartości zwracanej przez każde wywołanie metody):
const chaining = new MyChain();
console.log(chaining.add(3).subtract(1).value); // 2
lub jeśli po kolei wywołasz metody na oryginalnym obiekcie:
const notChaining = new MyChain();
notChaining.add(3);
notChaining.subtract(1);
console.log(notChaining.value) // 2
Jeśli chcesz, aby była między nimi różnica, możesz to pokazać, tworząc dwie wersje MyChain; taki, który działa tylko poprzez łańcuch, i taki, który działa tylko po kolei.
Poniższe wymaga tworzenia łańcucha, ponieważ nigdy nie aktualizuje oryginalnego obiektu, a wywołania metod zwracają nowe obiekty z wynikami wywołania metody:
class RealChain {
constructor(public value: number = 0) { }
sum(args: number[]) {
return new RealChain(args.reduce((s, c) => s + c, 0));
}
add(v: number) {
return new RealChain(this.value + v);
}
subtract(v: number) {
return new RealChain(this.value - v);
}
}
const realChaining = new RealChain();
console.log(realChaining.add(3).subtract(1).value); // 2
const notRealChaining = new RealChain();
notRealChaining.add(3);
notRealChaining.subtract(1);
console.log(notRealChaining.value) // 0
a poniższe zabrania tworzenia łańcucha , ponieważ aktualizuje tylko oryginalny obiekt, a jego metody niczego nie zwracają:
class NotChain {
value: number = 0;
constructor() {
this.value = 0;
}
sum(args: number[]) {
this.value = args.reduce((s, c) => s + c, 0);
}
add(v: number) {
this.value = this.value + v;
}
subtract(v: number) {
this.value = this.value - v;
}
}
const realNotChaining = new NotChain();
realNotChaining.add(3);
realNotChaining.subtract(1);
console.log(realNotChaining.value) // 2
const badNotChaining = new NotChain();
console.log(badNotChaining.add(3).subtract(1).value); // error!
// badNotChaining.add(3) is undefined so you can't call subtract() on it
Kod z forEach()działałby tylko z NotChaininstancjami, a nie z RealChaininstancjami.
Jeśli chcesz programistycznej pętli, która faktycznie działa z łańcuchem i bez wywoływania metod na oryginalnym obiekcie, prawdopodobnie powinieneś użyć reduce()zamiast forEach():
const realChainReduced = chainObj.reduce(
(mc, o) => mc[o.action](o.operand),
new RealChain() // or MyChain, doesn't matter
);
console.log("realChainReduced is", realChainReduced.value); // 10
Zauważ, że nie omówiłem żadnej z pozostałych części, w tym specyfiki języka TypeScript (użyte tutaj typy powodują błędy kompilatora), więc ostrzegam.
Link do kodu placu zabaw