Den første skal være sidst med JavaScript-arrays

Så det sidste skal være [0]og det første [længde - 1]. - Tilpasset fra Mattæus 20:16

Jeg springer over den malthusiske katastrofe og kommer til den: arrays er en af ​​de enkleste og vigtigste datastrukturer. Mens der ofte er adgang til terminalelementer (første og sidste), giver Javascript ingen praktisk egenskab eller metode til at gøre det, og brug af indekser kan være overflødige og tilbøjelige til bivirkninger og off-by-one-fejl.

Et mindre kendt, nyligt JavaScript TC39-forslag tilbyder trøst i form af to “nye” egenskaber: Array.lastItem& Array.lastIndex.

Javascript Arrays

I mange programmeringssprog inklusive Javascript er arrays nulindekseret. Tilslutningselementerne-første og underste sker via [0]og [length — 1]indekser, henholdsvis. Vi skylder denne fornøjelse et præcedens, der er angivet af C, hvor et indeks repræsenterer en forskydning fra hovedet på en matrix. Det gør nul til det første indeks, fordi det er arrayhovedet. Dijkstra proklamerede også "nul som et meget naturligt tal." Så lad det skrives. Så lad det ske.

Jeg formoder, at hvis du gennemsnitlig adgang efter indeks, ville du opdage, at der ofte henvises til terminalelementer. Når alt kommer til alt bruges arrays ofte til at gemme en sorteret samling og placerer superlative elementer (højeste, laveste, ældste, nyeste osv.) I enderne.

I modsætning til andre scripting-sprog (f.eks. PHP eller Elixir) giver Javascript ikke praktisk adgang til terminalarray-elementer. Overvej et trivielt eksempel på at bytte de sidste elementer i to arrays:

let faces = ["?", "?", "?", "?", "?"];let animals = ["?", "?", "?", "?", "?"]; 
let lastAnimal = animals[animals.length - 1];animals[animals.length - 1] = faces[faces.length - 1];faces[faces.length - 1] = lastAnimal;

Udskiftningslogikken kræver 2 arrays der refereres til 8 gange i 3 linjer! I den virkelige verdens kode kan dette hurtigt blive meget gentaget og vanskeligt for et menneske at analysere (selvom det er perfekt læsbart for en maskine).

Hvad mere er, kun ved hjælp af indekser kan du ikke definere en matrix og få det sidste element i det samme udtryk. Det virker måske ikke vigtigt, men overvej et andet eksempel, hvor funktionen getLogins(), foretager et asynkront API-opkald og returnerer et sorteret array. Forudsat at vi vil have den seneste loginhændelse i slutningen af ​​arrayet:

let lastLogin = async () => { let logins = await getLogins(); return logins[logins.length - 1];};

Medmindre længden er fast og kendt på forhånd, vi nødt til at tildele array til en lokal variabel at få adgang til sidste element. En almindelig måde at tackle dette på sprog som Python og Ruby er at bruge negative indekser. Derefter [length - 1]kan forkortes til [-1], hvilket fjerner behovet for lokal reference.

Jeg finder -1kun marginalt mere læselig end length — 1, og selvom det er muligt at tilnærme negative matrixindekser i Javascript med ES6 Proxy eller Array.slice(-1)[0], kommer begge med betydelige præstationsimplikationer for hvad der ellers skulle udgøre simpel tilfældig adgang.

Understreget & Lodash

Et af de mest kendte principper i softwareudvikling er Don't Repeat Yourself (DRY). Da adgang til terminalelementer er så almindelig, hvorfor ikke skrive en hjælperfunktion for at gøre det? Heldigvis leverer mange biblioteker som Underscore og Lodash allerede hjælpeprogrammer til _.first& _.last.

Dette giver en stor forbedring i lastLogin()eksemplet ovenfor:

let lastLogin = async () => _.last(await getLogins());

Men når det kommer til eksemplet med at bytte de sidste elementer, er forbedringen mindre signifikant:

let faces = ["?", "?", "?", "?", "?"];let animals = ["?", "?", "?", "?", "?"]; 
let lastAnimal = _.last(animals);animals[animals.length - 1] = _.last(faces);faces[faces.length - 1] = lastAnimal;

Disse hjælpefunktioner fjernede 2 af de 8 referencer, først nu introducerede vi en ekstern afhængighed, der mærkeligt nok ikke inkluderer en funktion til indstilling af terminalelementer.

En sådan funktion er sandsynligvis bevidst ekskluderet, fordi dens API ville være forvirrende og svær at læse. Tidlige versioner af Lodash gav en metode, _.last(array, n)hvor n var antallet af varer fra slutningen, men det blev i sidste ende fjernet til fordel for _.take(array, n).

Forudsat at der numser en række tal, hvad ville den forventede opførsel være af _.last(nums, n)? Det kunne returnere de to sidste elementer som _.take, eller det kunne indstille værdien af ​​det sidste element lig med n .

Hvis vi skulle skrive en funktion til indstilling af det sidste element i en matrix, er der kun få tilgange til at overveje at bruge rene funktioner, metodekædning eller brug af prototype:

let nums = ['d', 'e', 'v', 'e', 'l']; // set first = last
_.first(faces, _.last(faces)); // Lodash style
$(faces).first($(faces).last()); // jQuery style
faces.first(faces.last()); // prototype

Jeg finder ikke nogen af ​​disse tilgange til meget forbedring. Faktisk er noget vigtigt tabt her. Hver udfører en opgave, men ingen bruger tildelingsoperatoren ( =). Dette kunne gøres mere tydeligt med navngivningskonventioner som getLastog setFirst, men det bliver hurtigt alt for detaljeret. For ikke at nævne den femte helvedes cirkel er fuld af programmører, der er tvunget til at navigere i "selvdokumenterende" ældre kode, hvor den eneste måde at få adgang til eller ændre data er gennem getters og settere.

På en eller anden måde ser det ud til at vi sidder fast med [0]& [length — 1]...

Eller er vi? ?

Forslaget

Som nævnt forsøger et ECMAScript Technical Candidate (TC39) forslag at løse dette problem ved at definere to nye egenskaber på Arrayobjektet: lastItem& lastIndex. Dette forslag understøttes allerede i core-js 3 og kan bruges i dag i Babel 7 & TypeScript. Selvom du ikke bruger en transpiller, inkluderer dette forslag en polyfyldning.

Personligt finder jeg ikke meget værdi i lastIndexog foretrækker Rubys kortere navngivning af, firstog lastselvom dette blev udelukket på grund af mulige problemer med webkompatibilitet. Jeg er også overrasket over, at dette forslag ikke foreslår en firstItemegenskab til konsistens og symmetri.

I mellemtiden kan jeg tilbyde en Ruby-esque-tilgang uden afhængighed i ES6:

Først sidst

Vi har nu to nye Array-egenskaber - first& last–og en løsning, der:

✓ Bruger tildelingsoperatøren

✓ Kloner ikke arrayet

✓ Kan definere en matrix og få et terminalelement i et udtryk

✓ Er læsbar for mennesker

✓ Giver en grænseflade til hentning og indstilling

Vi kan omskrive lastLogin()igen i en enkelt linje:

let lastLogin = async () => (await getLogins()).last;

Men den virkelige gevinst kommer, når vi bytter de sidste elementer i to arrays med halvdelen af ​​antallet af referencer:

let faces = ["?", "?", "?", "?", "?"];let animals = ["?", "?", "?", "?", "?"]; 
let lastAnimal = animals.last;animals.last = faces.last;faces.last = lastAnimal;

Alt er perfekt, og vi har løst et af CS 'sværeste problemer. Der er ingen onde pagter, der gemmer sig i denne tilgang ...

Prototype Paranoia

Der er bestemt ingen [programmerer] på jorden så retfærdig at gøre godt uden nogensinde at synde. - Tilpasset fra Prædikeren 7:20

Mange overvejer at udvide et oprindeligt objekts prototype som et antimønster og en forbrydelse, der kan straffes med 100 års programmering i Java. Før indførelsen af enumerableejendommen Object.prototypekunne udvidelse ændre for insløjfernes adfærd . Det kan også føre til konflikt mellem forskellige biblioteker, rammer og tredjepartsafhængigheder.

Måske er det mest snigende problem, at en simpel stavefejl uden uforvarende kunne skabe et associerende array uden kompileringsværktøjer.

let faces = ["?", "?", "?", "?", "?"];let ln = faces.length 
faces.lst = "?"; // (5) ["?", "?", "?", "?", "?", lst: "?"] 
faces.lst("?"); // Uncaught TypeError: faces.lst is not a function 
faces[ln] = "?"; // (6) ["?", "?", "?", "?", "?", "?"] 

Denne bekymring er ikke unik for vores tilgang, den gælder for alle native Object-prototyper (inklusive arrays). Alligevel tilbyder dette sikkerhed i en anden form. Arrays i Javascript er ikke faste i længden, og der er derfor ingen IndexOutOfBoundsExceptions. Brug Array.lastsikrer, at vi ikke ved et uheld forsøger at få adgang til [length]og utilsigtet komme ind i undefinedterritorium.

Uanset hvilken tilgang du tager, er der faldgruber. Igen viser software sig at være en kunst til at gøre kompromiser.

Fortsat med den fremmede bibelske reference, forudsat at vi ikke tror, ​​at udvidelse Array.prototypeer en evig synd, eller hvis vi er villige til at tage en bid af den forbudte frugt, kan vi bruge denne kortfattede og læsbare syntaks i dag!

Sidste ord

Programmer skal skrives for folk at læse, og kun tilfældigt for maskiner at udføre. - Harold Abelson

I scripting-sprog som Javascript foretrækker jeg kode, der er funktionel, kortfattet og læsbar. Når det kommer til adgang til terminalarrayelementer, finder jeg Array.lastejendommen at være den mest elegante. I en produktionsfront-applikation foretrækker jeg muligvis Lodash for at minimere konflikter og bekymringer over browsere. Men i Node-back-end-tjenester, hvor jeg styrer miljøet, foretrækker jeg disse brugerdefinerede egenskaber.

Jeg er bestemt ikke den første, og heller ikke vil jeg være den sidste, der sætter pris på værdien (eller forsigtighed omkring implikationerne) af egenskaber som Array.lastItem, som forhåbentlig snart kommer til en version af ECMAScript i nærheden af ​​dig.

Følg mig på LinkedIn · GitHub · Medium