Zbuduj własną funkcję map w JavaScript

Mapowanie to proces przekształcania tablicy elementów w nową tablicę za pomocą mappera. W wyniku tego procesu otrzymujemy taką samą długość danych, jak podaliśmy. Spójrz na poniższą ilustrację. Ukazuje ona proces mapowania z podejściem deklaratywnym. Nie dyktujemy tutaj, w jaki sposób należy wykonać mapowanie, a raczej, co z nim zrobić.
Podajemy tablicę [?, ?, ?] i mapper cook oraz pozwalamy, aby funkcja map zajęła się iteracją po elementach tablicy i wywołała funkcję mapującą na każdym z nich. Na koniec dostaniemy nową tablicę [?, ?, ?].
Zdjęcie autora (zainspirowane Overflowjs)
Jeśli chodzi o mapowanie, mamy kilka opcji (zarówno z podejściem deklaratywnym, jak i imperatywnym).
Pętla For
Możemy użyć prostej pętli for
do przejścia po elementach tablicy:
let items = [1, 2, 3, 4, 5];
let double = (item) => item * 2;
const result = [];
for (let i = 0; i < items.length; i++) {
result.push(double(items[i]));
}
console.log(result);
// Result: [2, 4, 6, 8, 10]
Jak widać, spoczywa na nas zadanie śledzenia indeksów, inicjowania i przekazywania wyników do tablicy. Jest to wyraźny przykład programowania imperatywnego, które mówi komputerowi, w jaki sposób chcemy osiągnąć coś krok po kroku.
forEach
Inną opcją jest forEach
, która iteruje po każdym elemencie tablicy oddzielnie:
let items = [1, 2, 3, 4, 5];
let double = item => item * 2;
const result = [];
items.forEach(item => {
const doubledItem = double(item);
result.push(doubledItem);
});
console.log(result);
// Result: [2, 4, 6, 8, 10]
Wygląda lepiej, prawda? Nie musimy już śledzić indeksów elementów. Możemy się jednak zgodzić, że mutowanie elementu poza zasięgiem funkcji (w tym przypadku rezultatu) nie jest idealne. Byłoby super, gdybyśmy mogli znaleźć lepszą abstrakcję.
Lepszą alternatywą jest natywna mapa JavaScript.
Natywna mapa JS
Użyjmy natywnej metody mapy JavaScript. Wszystko, czego potrzebujemy to tablica danych i mapper. map
otrzyma tablicę i przeiteruje po każdym jej elemencie, jednocześnie stosując na nich funkcję mapera. Na koniec zwróci skonwertowaną tablicę o tej samej długości.
let items = [1, 2, 3, 4, 5];
let double = (item) => item * 2;
const result = items.map(double);
console.log(result);
// Result: [2, 4, 6, 8, 10]
Pod względem czytelności, jest to znacznie lepsze w porównaniu do forEach
lub pętli for
. Wydajność jest jednak bardzo istotna i trzeba ją wziąć pod uwagę, podejmując decyzję.
Budowanie własnej funkcji map
A teraz ta przyjemna część. Czy wiesz, że zbudowanie funkcji map
nie jest takie trudne? Zobaczmy, jak to działa.
Nasza funkcja map (wersja z pętlą for)
Tutaj ubieramy w abstrakcję śledzenie indeksu i inicjalizowanie tablicy. Wszystko, co musimy przekazać, to mapper i tablicę elementów.
let items = [1, 2, 3, 4, 5];
let double = (item) => item * 2;
// Loop Version of Map
let MapLoop = (fn, arr) => {
const mappedArr = [];
for (let i = 0; i < arr.length; i++) {
let mapped = fn(arr[i]);
mappedArr.push(mapped);
}
return mappedArr;
};
console.log(MapLoop(double, items));
// Result: [2, 4, 6, 8, 10]
Nasza funkcja map (wersja rekurencyjna)
Zbudowanie rekurencyjnej wersji funkcji map
jest interesujące. Jak to działa? Nadal przekazujemy zarówno mapper, jak i tablicę do funkcji, ale używamy ES6 destructuring assignment, aby rozbić tablicę na dwa parametry o nazwie head
oraz tail
.
Stosujemy podejście krok po kroku i rekurencyjnie wykonujemy mapper na każdym z elementów tablicy. W tym procesie używamy składni spread, aby połączyć wynik każdego wywołania MapRecursive
ze zmapowanym wynikiem fn(head)
.
Trwa to do momentu, gdy head
stanie się undefined
, co oznacza, że nie ma już więcej elementów w tablicy. Wtedy wydostaniemy się z funkcji rekurencyjnej pokazanej w line 8
i zaczynamy zwracać nową transformowaną tablicę.
let items = [1, 2, 3, 4, 5];
let double = (item) => item * 2;
// Recursive Version of Map
let MapRecursive = (fn, [head, ...tail]) => {
// bailout
if (head === undefined) {
return [];
}
return[fn(head), ...MapRecursive(fn, tail)];
};
console.log(MapRecursive(double, items));
// Step 1: head: 1, tail: [2,3,4,5], newArray: [2, ...MapRecursive(double, [2,3,4,5])]
// Step 2: head: 2, tail: [3,4,5], newArray: [2,4, ...MapRecursive(double, [3,4,5])]
// Step 3: head: 3, tail: [4,5], newArray: [2,4,6, ...MapRecursive(double, [4,5])]
// Step 4: head: 4, tail: [5], newArray: [2,4,6,8 ...MapRecursive(double, [5])]
// Step 5: head: 5, tail: [], newArray: [2,4,6,8,10 ...MapRecursive(double, [])]
// Step 6: head: undefined -> return newArray: [2,4,6,8,10]
Nasz funkcja map (wersja z generatorem)
Możesz także zbudować funkcję mapy za pomocą funkcji generatora. Nie jest to idealny sposób na obsługę mapowania i nie daje takiego samego rezultatu jak poprzednie przykłady, ponieważ funkcje generatora zwracają iterator.
Ta wersja ma charakter edukacyjny, żebyśmy mogli zobaczyć, jak podobną koncepcję można zastosować również w funkcjach generatora.
Poniżej możesz zobaczyć końcowy wynik wywołania MapGenerator:
let items = [1, 2, 3, 4, 5];
let double = (item) => item * 2;
// Generator version of Map
let MapGenerator = function * (fn, arr) {
for (let x of arr) {
yield fn(x);
}
};
const result = MapGenerator(double, items);
console.log(result.next());
// Object {value: 2, done: false}
console.log(result.next());
// Object {value: 4, done: false}
console.log(result.next());
// Object {value: 6, done: false}
console.log(result.next());
// Object {value: 8, done: false}
console.log(result.next());
// Object {value: 10, done: false}
console.log(result.next());
// Object {value: undefined, done: true}
Źródła
- https://www.freecodecamp.org/news/implement-array-map-with-recursion-35976d0325b2/
- https://www.digitalocean.com/community/tutorials/list-processing-with-map-filter-and-reduce