Vad de inte berättade om ES5 s Array Extras

Varje ny version av JavaScript lägger till några extra godsaker som gör programmeringen enklare. EcmaScript 5 lade till några mycket nödvändiga metoder för Array datatyp, och medan du kan hitta resurser som lär dig hur du använder dessa metoder, slipper de vanligtvis en diskussion om att använda dem med något annat än en tråkig, anpassad funktion.

Alla array extras ignorerar hål i arrays.

De nya matningsmetoder som läggs till i ES5 kallas vanligtvis som Array Extras. De underlättar arbetet med att arbeta med arrays genom att tillhandahålla metoder för att utföra gemensamma operationer. Här är en nästan komplett lista över de nya metoderna:

  • Array.prototype.map
  • Array.prototype.reduce
  • Array.prototype.reduceRight
  • Array.prototype.filter
  • Array.prototype.forEach
  • Array.prototype.every
  • Array.prototype.some

Array.prototype.indexOf och Array.prototype.lastIndexOf är också en del av den listan, men denna handledning kommer bara att diskutera ovanstående sju metoder.


Vad de berättade för dig

Dessa metoder är ganska enkla att använda. De utför en funktion som du tillhandahåller som deras första argument, för varje element i matrisen. Vanligtvis skulle den levererade funktionen ha tre parametrar: elementet, elementets index och hela arrayen. Här är några exempel:

[1, 2, 3] .map (funktion (elem, index, arr) return elem * elem;); // returnerar [1, 4, 9] [1, 2, 3, 4, 5] .filter (funktion (elem, index, arr) return elem% 2 === 0;); // returnerar [2, 4] [1, 2, 3, 4, 5] .some (funktion (elem, index, arr) return elem> = 3;); // returnerar sant [1, 2, 3, 4, 5]. varje (funktion (elem, index, arr) return elem> = 3;); // returnerar false

De minska och reduceRight metoder har en annan parameterlista. Som deras namn föreslår, reducerar de en array till ett enda värde. Det ursprungliga värdet av resultatet är standardvärdet till det första elementet i matrisen, men du kan skicka ett andra argument till dessa metoder för att tjäna som initialvärdet.

Återuppringningsfunktionen för dessa metoder accepterar fyra argument. Det aktuella tillståndet är det första argumentet, och de återstående argumenten är elementet, indexet och arrayen. Följande fragment visar användningen av dessa två metoder:

[1, 2, 3, 4, 5] .reduce (funktion (summa, elem, index, arr) return sum + elem;); // returnerar 15 [1, 2, 3, 4, 5] .reduce (funktion (summa, elem, index, arr) return sum + elem;, 10); // returnerar 25

Men du visste säkert redan allt detta, eller hur? Så låt oss gå över på något du kanske inte känner till.


Funktionell programmering till räddningen

Det är förvånande att fler människor inte vet det här: du behöver inte skapa en ny funktion och överföra den till .Karta() och vänner. Ännu bättre kan du skicka inbyggda funktioner, till exempel parseFloat utan omslag krävs!

["1", "2", "3", "4"]. Karta (parseFloat); // returnerar [1, 2, 3, 4]

Observera att vissa funktioner inte fungerar som förväntat. Till exempel, parseInt accepterar en radix som ett andra argument. Kom nu ihåg att elementets index överförs till funktionen som ett andra argument. Så vad kommer följande att återvända?

["1", "2", "3", "4"]. Karta (parseInt);

Exakt: [1, NaN, NaN, NaN]. Som en förklaring ignoreras bas 0; så det första värdet analyseras som förväntat. Följande baser inkluderar inte numret som passerat som första argumentet (t.ex. bas 2 innehåller inte 3), vilket leder till NaNs. Så var noga med att kolla Mozilla Developer Network innan du använder en funktion och du kommer att vara bra att gå.

Proffstips: Du kan även använda inbyggda konstruktörer som argument, eftersom de inte är skyldiga att ringas med ny. Som ett resultat kan en enkel omvandling till ett booleskt värde göras med Boolean, så här:

["ja", 0, "nej", "", "sant", "falskt"). filter (booleska); // returnerar ["ja", "nej", "sant", "falskt"]

Ett par andra fina funktioner är encodeURIComponent, Date.parse (Observera att du inte kan använda Datum konstruktör eftersom det alltid returnerar det aktuella datumet när det kallas utan ny), Array.isArray och JSON.parse.


Glöm inte att .tillämpa()

Medan du använder inbyggda funktioner som argument för array-metoder kan det vara en bra syntax, bör du också komma ihåg att du kan överföra en array som det andra argumentet om Function.prototype.apply. Detta är praktiskt, när man ringer till metoder, som Math.max eller String.fromCharCode. Båda funktionerna accepterar ett variabelt antal argument, så du måste sätta ihop dem i en funktion när du använder array-extrafunktionerna. Så istället för:

var ar = [1, 2, 4, 5, 3]; var max = arr.reduce (funktion (a, b) returnera Math.max (a, b););

Du kan skriva följande:

var ar = [1, 2, 4, 5, 3]; var max = Math.max.apply (null, arr);

Denna kod kommer också med en bra prestationsförmån. Som sidnot: I EcmaScript 6 kan du enkelt skriva:

var ar = [1, 2, 4, 5, 3]; var max = Math.max (... ar); // DENNA HELT FUNGERAR INTE!

Hole-less Arrays

Alla array extras ignorerar hål i arrays. Ett exempel:

var a = ["hej",,,, "värld"]; // a [1] till en [4] är inte definierad var count = a.reduce (funktion (count) return count + 1;, 0); console.log (count); // 2

Detta beteende kommer förmodligen med en prestationsförmån, men det finns fall där det kan vara en verklig smärta i rumpan. Ett sådant exempel kan vara när du behöver en rad slumpmässiga nummer; det är inte möjligt att helt enkelt skriva det här:

var randomNums = new Array (5) .map (Math.random);

Men kom ihåg att du kan ringa alla inbyggda konstruktörer utan ny. Och en annan användbar tidbit: Function.prototype.apply ignorerar inte hål. Kombinera dessa, returnerar den här koden det rätta resultatet:

var randomNums = Array.apply (null, ny Array (5)). karta (Math.random);

Det okända andra argumentet

De flesta av ovanstående är kända och används regelbundet av många programmerare. Vad de flesta av dem inte vet (eller åtminstone inte använder) är det andra argumentet för de flesta av extrafunktionerna (endast minska* funktioner stöder det inte).

Med det andra argumentet kan du skicka en detta värdet till funktionen. Som ett resultat kan du använda prototyp-metoder. Filtrering av en array med ett ordinärt uttryck blir till exempel en enfärgad fil:

["foo", "bar", "baz"] .filter (RegExp.prototype.test, / ^ b /); // returnerar ["bar", "baz"]

Dessutom kontrolleras om ett objekt har vissa egenskaper blir en cinch:

["foo", "isArray", "skapa"]. några (Object.prototype.hasOwnProperty, Object); // returnerar sant (på grund av Object.create)

Till sist kan du använda alla metoder du vill:

// kan göra något galet [funktion (a) returnera a * a; , funktion (b) returnera b * b * b; ]. mapp (Array.prototype.map, [1, 2, 3]); // returnerar [[1, 4, 9], [1, 8, 27]]

Detta blir galet när du använder Function.prototype.call. Kolla på detta:

["foo", "\ n \ tbar", "\ r \ nbaz \ t"] .map (Function.prototype.call, String.prototype.trim); // returnerar ["foo", "bar", "baz"] [sann, 0, null, []]. karta (Function.prototype.call, Object.prototype.toString); // returnerar ["[objekt Boolean]", "[objektnummer]", "[objekt Null]", "[Object Array]"

Naturligtvis, för att behaga din inre nörd, kan du också använda Function.prototype.call som den andra parametern. När det görs kallas varje element i arrayen med sitt index som första argumentet och hela arrayen som andra:

[funktion (index, arr) // vad du kanske vill göra med det]. forEach (Function.prototype.call, Function.prototype.call);

Låt oss bygga något användbart

Med allt sagt, låt oss bygga en enkel kalkylator. Vi vill bara stödja de grundläggande operatörerna (+, -, *, /), och vi måste respektera operatörsförfarandet. Så multiplikation (*) och division (/) måste utvärderas före tillsats (+) och subtraktion (-).

För det första definierar vi en funktion som accepterar en sträng som representerar beräkningen som det första och enda argumentet.

funktion beräkna (beräkning) 

I funktionskroppen börjar vi konvertera beräkningen till en array genom att använda ett reguljärt uttryck. Då ser vi till att vi analyserade hela beräkningen genom att gå med i delarna med Array.prototype.join och jämföra resultatet med den ursprungliga beräkningen.

var parts = calculation.match (// siffror | operatörer | whitespace /(?:\-?[\d\.]+)|[-\+\*\/]|s +/g); om (beräkning! == parts.join ("")) kasta nytt fel ("kunde inte analysera beräkning")

Därefter ringer vi String.prototype.trim för varje element för att eliminera white-space. Sedan filtrerar vi arrayen och tar bort falsey-element (dvs: f tomma strängar).

delar = parts.map (Function.prototype.call, String.prototype.trim); delar = parts.filter (Boolean);

Nu bygger vi en separat grupp som innehåller parsade nummer.

var nums = parts.map (parseFloat);

Du kan skicka inbyggda funktioner som parseFloat utan omslag krävs!

Vid det här laget är det enklaste sättet att fortsätta för-slinga. Inom den bygger vi en annan matris (namngiven bearbetas) med multiplikation och division redan tillämpad. Grundidén är att minska varje operation till ett tillägg, så att sista steget blir ganska trivialt.

Vi kontrollerar alla delar av nums array för att säkerställa att det inte är det NaN; om det inte är ett nummer, då är det en operatör. Det enklaste sättet att göra detta är att dra nytta av det faktum att i JavaScript, NaN! == NaN. När vi hittar ett nummer lägger vi till det i resultatmatrisen. När vi hittar en operatör, tillämpar vi den. Vi hoppar över tilläggsoperationer och ändrar bara tecknet på nästa tal för subtraktion.

Multiplikation och delning måste beräknas med hjälp av de två omgivande talen. Eftersom vi redan bifogade det föregående numret till matrisen måste det tas bort med Array.prototype.pop. Resultatet av beräkningen läggs till i resultatmatrisen, redo att läggas till.

var bearbetad = []; för (var i = 0; i < parts.length; i++) if( nums[i] === nums[i] ) processed.push( nums[i] );  else  switch( parts[i] )  case "+": continue; //ignore case "-": processed.push(nums[++i] * -1); break; case "*": processed.push(processed.pop() * nums[++i]); break; case "/": processed.push(processed.pop() / nums[++i]); break; default: throw new Error("unknown operation: " + parts[i]);   

Det sista steget är ganska enkelt: Vi lägger bara till alla nummer och returnerar vårt slutresultat.

returnera processed.reduce (funktion (resultat, elem) returresultat + elem;);

Den färdiga funktionen ska se ut så här:

funktionen beräkna (beräkning) // bygga en grupp som innehåller de enskilda delarna var delar = calculation.match (// siffror | operatörer | whitespace / (?: \ -? [\ d \.] +) | [- \ + \ * \ /] | \ s + / g); // test om allt var matchat om (beräkning! == parts.join ("")) kasta nytt fel ("kunde inte analysera beräkning") // ta bort alla whitespace-delar = parts.map (Function.prototype. ring, String.prototype.trim); delar = parts.filter (Boolean); // skapa en separat uppsättning som innehåller parsade nummer var nums = parts.map (parseFloat); // bygga en annan array med alla operationer reducerade till additioner var processed = []; för (var i = 0; i < parts.length; i++) if( nums[i] === nums[i] ) //nums[i] isn't NaN processed.push( nums[i] );  else  switch( parts[i] )  case "+": continue; //ignore case "-": processed.push(nums[++i] * -1); break; case "*": processed.push(processed.pop() * nums[++i]); break; case "/": processed.push(processed.pop() / nums[++i]); break; default: throw new Error("unknown operation: " + parts[i]);    //add all numbers and return the result return processed.reduce(function(result, elem) return result + elem; ); 

Okej, så låt oss testa det:

beräkna ("2 + 2,5 * 2") // returnerar 7 beräkna ("12/6 + 4 * 3") // returnerar 14

Det verkar fungera! Det finns fortfarande några kantfall som inte hanteras, till exempel operatörens första beräkningar eller siffror som innehåller flera punkter. Stöd för parentes skulle vara trevligt, men vi kommer inte oroa dig för att gräva i mer detaljer i det här enkla exemplet.


Avslutar

Medan ES5s array-extras kan tyckas vara ganska triviala, visar de ganska lite djup när du ger dem en chans. Plötsligt blir funktionell programmering i JavaScript mer än callback helvete och spagetti kod. Att realisera detta var en riktig ögonöppnare för mig och påverkat mitt sätt att skriva program.

Självklart är det som sagt ovan alltid fall där du istället vill använda en vanlig slinga. Men, och det är den fina delen, du behöver inte.