Elementer af JavaScript-stil

Out of the Blue - Iñaki Bolumburu (CC BY-NC-ND 2.0)
Bemærk: Dette indlæg er nu en del af bogen "Composing Software".

I 1920 blev "The Elements of Style" af William Strunk Jr offentliggjort, der opstod retningslinjer for den engelsksprogede stil, der har varet testen af ​​tiden. Du kan forbedre din kode ved at anvende lignende standarder på din kodestil.

Følgende er retningslinjer, ikke uforanderlige love. Der kan være gyldige grunde til at afvige fra dem, hvis du gør det klarere kodens betydning, men vær årvågen og selvbevidst. Disse retningslinjer har afprøvet tiden med god grund: De har normalt ret. Afvig kun fra dem med god grund - ikke blot med et indfald eller en personlig stilpræference.

Næsten enhver retningslinje fra komponentets grundlæggende principper gælder for kildekode:

  • Gør afsnittet til kompositionenheden: Et afsnit til hvert emne.
  • Udelad unødvendige ord.
  • Brug aktiv stemme.
  • Undgå en række løse sætninger.
  • Hold relaterede ord sammen.
  • Sæt udsagn i positiv form.
  • Brug parallel konstruktion på parallelle koncepter.

Vi kan anvende næsten identiske koncepter til kodestil:

  1. Gør funktionen til kompositionenheden. Et job til hver funktion.
  2. Udelad unødvendig kode.
  3. Brug aktiv stemme.
  4. Undgå en række løse udsagn.
  5. Opbevar den relaterede kode sammen.
  6. Sæt udsagn og udtryk i positiv form.
  7. Brug parallel kode til parallelle koncepter.

1. Gør funktionen til kompositionenheden. Et job til hver funktion.

Essensen af ​​softwareudvikling er sammensætning. Vi bygger software ved at komponere moduler, funktioner og datastrukturer sammen.
At forstå, hvordan man skriver og komponerer funktioner, er en grundlæggende færdighed for softwareudviklere.

Moduler er simpelthen samlinger af en eller flere funktioner eller datastrukturer, og datastrukturer er, hvordan vi repræsenterer programtilstand, men der sker intet interessant, før vi anvender en funktion.

I JavaScript er der tre slags funktioner:

  • Kommunikationsfunktioner: Funktioner, der udfører I / O.
  • Procedurefunktioner: En liste over instruktioner, samlet.
  • Kortlægningsfunktioner: Givet nogle input, returner nogle tilsvarende output.

Mens alle nyttige programmer har brug for I / O, og mange programmer følger nogle proceduremæssige sekvenser, skal størstedelen af ​​dine funktioner være kortlægningsfunktioner: Givet nogle input, returnerer funktionen nogle tilsvarende output.

En ting for hver funktion: Hvis din funktion er for I / O, skal du ikke blande denne I / O med kortlægning (beregning). Hvis din funktion er til kortlægning, skal du ikke blande den med I / O. Principielt er procedurefunktioner i strid med denne retningslinje. Procedurefunktioner er også i strid med en anden retningslinje: Undgå en række løse udsagn.

Den ideelle funktion er en enkel, deterministisk, ren funktion:

  • Givet den samme input, returner altid den samme output
  • Ingen bivirkninger

Se også "Hvad er en ren funktion?"

2. Udelad unødvendig kode.

”Kraftig skrivning er kortfattet. En sætning bør ikke indeholde unødvendige ord, et afsnit ingen unødvendige sætninger af samme grund, at en tegning ikke skal have unødvendige linjer og en maskine ingen unødvendige dele. Dette kræver ikke, at forfatteren kortlægger alle sætninger eller undgår alle detaljer og behandler emner kun i disposition, men at hvert ord fortæller det. ”[Unødvendige ord udeladt.]
~ William Strunk, Jr., The Elements of Style

Kortfattet kode er kritisk i software, fordi mere kode skaber mere overfladeareal for fejl at gemme sig i. Mindre kode = færre steder for fejl at skjule = færre fejl.

Den kortfattede kode er mere læselig, fordi den har et højere signal / støj-forhold: Læseren skal sile gennem mindre syntaksstøj for at nå meningen. Mindre kode = mindre syntaksstøj = stærkere signal til betydning for transmission.

At låne et ord fra The Elements of Style: Den kortfattede kode er mere energisk.

funktionshemmelighed (besked) {
  returfunktion () {
    retur besked;
  }
};

Kan reduceres til:

const secret = msg => () => msg;

Dette er meget mere læseligt for dem, der kender kortfattede pilefunktioner (introduceret i 2015 med ES6). Det udelader unødvendig syntaks: seler, funktionens nøgleord og returneringssætningen.

Den første inkluderer unødvendig syntaks. Klammeparentes, nøgleordet til funktionen og returneringserklæringen tjener intet formål for dem, der kender kortfattet pilens syntaks. Den findes kun for at gøre koden kendt for dem, der endnu ikke er flydende med ES6.

ES6 har været sprogstandarden siden 2015. Det er tid til at blive fortrolig.

Udelad unødvendige variabler

Nogle gange er vi fristet til at tildele navne til ting, der ikke behøver at blive navngivet. Problemet er, at den menneskelige hjerne har et begrænset antal ressourcer, der er tilgængelige i arbejdshukommelsen, og hver variabel skal opbevares som et diskret kvanta, der besætter en af ​​de tilgængelige arbejdshukommelseslot.

Af denne grund lærer erfarne udviklere at eliminere variabler, som ikke behøver at eksistere.

I de fleste situationer skal du for eksempel udelade variabler, der kun er oprettet for at navngive returneringsværdier. Navnet på din funktion skal give tilstrækkelig information om, hvad funktionen vender tilbage. Overvej følgende:

const getFullName = ({firstName, lastName}) => {
  const fullName = fornavn + '' + efternavn;
  returner fuldnavn;
};

vs ...

const getFullName = ({firstName, lastName}) => (
  fornavn + '' + efternavn
);

En anden almindelig måde, hvorpå udviklere kan reducere variabler, er at drage fordel af funktionskomposition og punktfri stil.

Punktfri stil er en måde at definere funktioner uden at henvise til de argumenter, som funktionerne fungerer på. Almindelige måder at opnå punktfri stil inkluderer currying og funktion sammensætning.

Lad os se på et eksempel ved hjælp af curry:

const add2 = a => b => a + b;
// Nu kan vi definere et punktfrit inc ()
// der tilføjer 1 til ethvert nummer.
const inc = add2 (1);
inc (3); // 4

Se på definitionen af ​​inc () -funktionen. Bemærk, at det ikke bruger funktionsnøgleordet eller syntaxen =>. Der er ikke noget sted at liste parametre, fordi funktionen ikke bruger en parameterliste internt. I stedet returnerer det en funktion, der ved, hvordan man skal håndtere argumenter.

Lad os se på et andet eksempel ved hjælp af funktionskomposition. Funktionssammensætning er processen med at anvende en funktion på resultatet af en anden funktionsanvendelse. Uanset om du er klar over det eller ej, bruger du funktionskomposition hele tiden. Du bruger det, når du for eksempel kæder metoder som .map () eller lover.then (). I dets mest basale form ser det sådan ud: f (g (x)). I algebra er denne komposition normalt skrevet f ∘ g (ofte udtalt, “f efter g” eller “f komponeret med g”).

Når du sammensætter to funktioner sammen, fjerner du behovet for at oprette en variabel til at indeholde mellemværdien mellem de to funktionsapplikationer. Lad os se, hvordan funktionskomposition kan rydde op i en kode:

const g = n => n + 1;
const f = n => n * 2;
// Med point:
const incThenDoublePoints = n => {
  const inkremented = g (n);
  retur f (inkrementeret);
};
incThenDoublePoints (20); // 42
// compose2 - Tag to funktioner, og returner deres komposition
const compose2 = (f, g) => x => f (g (x));
// Punktfri:
const incThenDoublePointFree = compose2 (f, g);
incThenDoublePointFree (20); // 42

Du kan gøre det samme med enhver funktor. En funktor er alt, hvad du kan kortlægge, f.eks. Arrays (Array.map ()) eller løfter (lover.then ()). Lad os skrive en anden version af compose2 ved hjælp af kortkæde til funktionskomposition:

const compose2 = (f, g) => x => [x] .map (g) .map (f) .pop ();
const incThenDoublePointFree = compose2 (f, g);
incThenDoublePointFree (20); // 42

Du gør meget det samme, hver gang du bruger løftekæder.

Stort set hvert funktionelt programmeringsbibliotek har mindst to versioner af kompositionsværktøjer: komponere (), der anvender funktioner fra højre til venstre, og pipe (), der anvender funktioner fra venstre til højre.

Lodash kalder dem komponere () og flow (). Når jeg bruger dem fra Lodash, importerer jeg det altid sådan:

importrør fra 'lodash / fp / flow';
rør (g, f) (20); // 42

Dette er dog ikke meget mere kode, og det gør den samme ting:

const pipe = (... fns) => x => fns.reduce ((acc, fn) => fn (acc), x);
rør (g, f) (20); // 42

Hvis det med denne funktionskomposition lyder fremmed for dig, og du ikke er sikker på, hvordan du ville bruge det, skal du tænke nøje over dette:

Essensen af ​​softwareudvikling er sammensætning. Vi bygger applikationer ved at komponere mindre moduler, funktioner og datastrukturer.

Herfra kan du konkludere, at forståelse af værktøjerne til funktion og objektsammensætning er lige så grundlæggende som en hjemmebygger, der forstår bor og sømpistoler.

Når du bruger ufravigelig kode til at dele funktioner med mellemliggende variabler, er det som at komponere disse stykker med duct tape og skørt lim.

Husk:

  • Hvis du kan sige det samme med mindre kode, uden at ændre eller tilsløre betydningen, skal du.
  • Hvis du kan sige det samme med færre variabler, uden at ændre eller tilsløre betydningen, skal du.

3. Brug Aktiv stemme

”Den aktive stemme er normalt mere direkte og energisk end den passive.” ~ William Strunk, Jr., The Elements of Style

Navngiv tingene så direkte som muligt.

  • myFunction.wasCalled () er bedre end myFunction.hasBeenCalled ()
  • createUser () er bedre end User.create ()
  • underrette () er bedre end Notifier.doNotification ()

Navn predikater og booleaner, som om de er ja eller ingen spørgsmål:

  • isActive (bruger) er bedre end getActiveStatus (bruger)
  • isFirstRun = falsk; er bedre end firstRun = falsk;

Navnfunktioner ved hjælp af verbformer:

  • forøgelse () er bedre end plusOne ()
  • unzip () er bedre end filesFromZip ()
  • filter (fn, array) er bedre end matchingItemsFromArray (fn, array)

Event Handlers

Begivenhedshåndterere og livscyklusmetoder er en undtagelse fra verbreglen, fordi de bruges som kvalifikatorer; i stedet for at udtrykke, hvad de skal gøre, udtrykker de, hvornår de skal gøre det. De skal navngives, så de læser, ", ".

  • element.onClick (handleClick) er bedre end element.click (handleClick)
  • component.onDragStart (handleDragStart) er bedre end component.startDrag (handleDragStart)

I de anden former ser det ud til, at vi prøver at udløse begivenheden i stedet for at svare på den.

Livscyklusmetoder

Overvej følgende alternativer til en komponent hypotetisk livscyklusmetode, der findes for at kalde en handlerfunktion, før en komponent opdateres:

  • componentWillBeUpdated (doSomething)
  • componentWillUpdate (doSomething)
  • beforeUpdate (doSomething)

I det første eksempel bruger vi passiv stemme (opdateres i stedet for vil opdateres). Det er en mundfuld og ikke mere klar end de andre alternativer.

Det andet eksempel er meget bedre, men hele pointen med denne livscyklusmetode er at kalde en operatør. componentWillUpdate (handler) læser som om den vil opdatere behandleren, men det er ikke hvad vi mener. Vi mener, "inden komponenten opdateres, skal du ringe til behandleren". beforeComponentUpdate () udtrykker intentionen mere tydeligt.

Vi kan forenkle endnu mere. Da dette er metoder, er emnet (komponenten) indbygget. At henvise til det i metodenavnet er overflødigt. Tænk over, hvordan det ville læse, hvis du kaldte disse metoder direkte: component.componentWillUpdate (). Det er som at sige, "Jimmy Jimmy vil have bøf til middag." Du behøver ikke at høre emnets navn to gange.

  • component.beforeUpdate (doSomething) er bedre end component.beforeComponentUpdate (doSomething)

Funktionelle mixins er funktioner, der tilføjer egenskaber og metoder til et objekt. Funktionerne anvendes efter hinanden i en rørledning - som en samlebånd. Hver funktionel mixin tager forekomsten som et input og klæber nogle ting på det, før det overføres til den næste funktion i rørledningen.

Jeg kan godt lide at navngive funktionelle mixins ved hjælp af adjektiver. Du kan ofte bruge “ing” eller “stand” -suffikser for at finde nyttige adjektiver. Eksempler:

  • const duck = composeMixins (flyve, kvæve);
  • const box = composeMixins (iterable, mappable);

4. Undgå en række løse udsagn

"... en serie bliver snart monoton og kedelig."
~ William Strunk, Jr., The Elements of Style

Udviklere strenger ofte sammen sekvenser af begivenheder i en procedure: en gruppe af løst relaterede udsagn designet til at køre den ene efter den anden. Et overskud af procedurer er en opskrift på spaghettikode.

Sådanne sekvenser gentages ofte ved mange parallelle former, hver af dem subtilt og undertiden uventet divergerende. For eksempel deler en brugergrænsefladekomponent de samme kernebehov med stort set alle andre brugergrænsefladekomponenter. Dets bekymringer kan opdeles i livscyklusfaser og styres af separate funktioner.

Overvej følgende rækkefølge:

const drawUserProfile = ({userId}) => {
  const userData = loadUserData (userId);
  const dataToDisplay = beregneDisplayData (userData);
  renderProfileData (dataToDisplay);
};

Denne funktion håndterer virkelig tre forskellige ting: indlæsning af data, beregning af visningstilstand fra indlæste data og gengivelse.

I de fleste moderne frontend-applikationsarkitekturer betragtes hver af disse bekymringer separat. Ved at adskille disse bekymringer kan vi let blande og matche forskellige funktioner for hver bekymring.

For eksempel kunne vi erstatte rendereren fuldstændigt, og det ville ikke påvirke de andre dele af programmet, f.eks. React's rigdom af tilpassede rendere: ReactNative til indbyggede iOS- og Android-apps, AFrame til WebVR, ReactDOM / Server til server-side-gengivelse , etc…

Et andet problem med denne funktion er, at du ikke blot kan beregne de data, der skal vises, og generere markeringen uden først at indlæse dataene. Hvad hvis du allerede har indlæst dataene? Du ender med at udføre arbejde, som du ikke havde brug for i de efterfølgende opkald.

At adskille bekymringerne gør dem også uafhængigt testbare. Jeg kan godt lide at enhedstest mine applikationer og vise testresultater med hver ændring, når jeg skriver koden. Hvis vi imidlertid binder renderingskode til dataindlæsningskode, kan jeg ikke blot overføre nogle falske data til renderingskoden til testformål. Jeg er nødt til at teste hele komponenten fra ende til ende - en proces, der kan være tidskrævende på grund af browserindlæsning, asynkron netværks I / O osv.…

Jeg får ikke øjeblikkelig feedback fra mine enhedstest. Ved at adskille funktionerne kan du teste enheder isoleret fra hinanden.

Dette eksempel har allerede separate funktioner, som vi kan tilføre til forskellige livscykluskroge i applikationen. Indlæsning kan udløses, når komponenten monteres af applikationen. Beregning og gengivelse kan ske som svar på at se statusopdateringer.

Resultatet er software med ansvar, der er tydeligere afgrænset: Hver komponent kan genbruge den samme struktur og livscykluskroge, og softwaren fungerer bedre; vi gentager ikke arbejde, der ikke behøver at blive gentaget i efterfølgende cykler.

5. Opbevar den relaterede kode sammen.

Mange rammer og kedelplader foreskriver en metode til programorganisation, hvor filer er grupperet efter teknisk type. Dette er fint, hvis du bygger en lille lommeregner eller en To Do-app, men til større projekter er det normalt bedre at gruppere filer efter funktion.

Her er for eksempel to alternative filhierarkier til en opgaveapp-app efter type og funktion:

Grupperet efter type:

.
├── komponenter
│ ├── todos
│ └── bruger
├── reduktionsredskaber
│ ├── todos
│ └── bruger
└── test
    ├── todos
    └── bruger

Grupperet efter funktion:

.
├── todos
│ ├── komponent
│ ├── reducer
│ └── test
└── bruger
    ├── komponent
    ├── reducer
    └── test

Når du grupperer filer efter funktion, undgår du at rulle op og ned i din filliste for at finde alle de filer, du har brug for at redigere for at få en enkelt funktion til at fungere.

Colocate filer relateret efter funktion.

6. Sæt udsagn og udtryk i positiv form.

”Lav konkrete påstande. Undgå tamt, farveløst, tøvende, ikke-forpligtende sprog. Brug ordet ikke som et middel til benægtelse eller i antithese, aldrig som et middel til at undgå. ”
~ William Strunk, Jr., The Elements of Style
  • isFlying er bedre end isNotFlying
  • sent er bedre end notOnTime

Hvis udsagn

hvis (fejle) returnere afvise (fejle);
// gør noget...

…er bedre end:

hvis (! fejlagtigt) {
  // ... gør noget
} andet {
  vende tilbage (fejle);
}

Ternaries

{
  [Symbol.iterator]: iterator? iterator: defaultIterator
}

…er bedre end:

{
  [Symbol.iterator]: (! Iterator)? defaultIterator: iterator
}

Foretrækker stærke negative udsagn

Nogle gange bryder vi os kun om en variabel, hvis den mangler, så at bruge et positivt navn ville tvinge os til at negere den med! operatør. I disse tilfælde foretrækker du en stærk negativ form. Ordet "ikke" og! operatør skaber svage udtryk.

  • hvis (missingValue) er bedre end hvis (! hasValue)
  • hvis (anonym) er bedre end hvis (! bruger)
  • hvis (isEmpty (ting)) er bedre end hvis (notDefined (ting))

Undgå nul og ikke-definerede argumenter i funktionsopkald

Kræver ikke, at funktionskaldere videregiver udefineret eller nul i stedet for en valgfri parameter. Foretrækker navngivne indstillingsobjekter i stedet:

const createEvent = ({
  title = 'Untitled',
  timeStamp = Date.now (),
  beskrivelse = ''
}) => ({titel, beskrivelse, tidstempel});
// senere...
const birthdayParty = createEvent ({
  titel: 'Fødselsdagsfest',
  beskrivelse: 'Bedste fest nogensinde!'
});

…er bedre end:

const createEvent = (
  title = 'Untitled',
  timeStamp = Date.now (),
  beskrivelse = ''
) => ({titel, beskrivelse, timeStamp});
// senere...
const birthdayParty = createEvent (
  'Fødselsdagsfest',
  undefined, // Dette kunne undgås
  'Bedste fest nogensinde!'
);

Brug parallelkode til parallelle koncepter

”... parallel konstruktion kræver, at udtryk for lignende indhold og funktion skal være ydre ens. Formens lighed giver læseren mulighed for lettere at genkende lighed med indhold og funktion. ”
~ William Strunk, Jr., The Elements of Style

Der er få problemer i applikationer, der ikke er løst før. Vi ender med at gøre de samme ting igen og igen. Når det sker, er det en mulighed for abstraktion. Identificer de dele, der er ens, og opbyg en abstraktion, der giver dig mulighed for kun at levere de dele, der er forskellige. Det er præcis, hvad biblioteker og rammer gør for os.

UI-komponenter er et godt eksempel. For mindre end et årti siden var det almindeligt at blande UI-opdateringer ved hjælp af jQuery med applikationslogik og netværks I / O. Derefter begyndte folk at indse, at vi kunne anvende MVC til webapps på klientsiden, og folk begyndte at adskille modeller fra UI-opdateringslogik.

Til sidst landede webapps på en komponentmodeltilgang, som lader os erklærende modellere vores komponenter ved hjælp af ting som JSX eller HTML-skabeloner.

Hvad vi endte med, er en måde at udtrykke UI-opdateringslogik på samme måde for hver komponent snarere end forskellige imperativkoder for hver enkelt.

For dem, der kender komponenter, er det temmelig let at se, hvordan hver komponent fungerer: Der er nogle deklarerende markup, der udtrykker UI-elementer, begivenhedshåndterere til at tilslutte adfærd og livscykluskroge til at vedhæfte tilbagekald, der kører, når vi har brug for dem.

Når vi gentager lignende mønstre for lignende problemer, skal enhver, der er bekendt med mønsteret, hurtigt kunne lære, hvad koden gør.

Konklusion: Kode skal være enkel, ikke forenklet

Kraftig skrivning er kortfattet. En sætning bør ikke indeholde unødvendige ord, et afsnit ingen unødvendige sætninger af samme grund, at en tegning ikke skal have unødvendige linjer og en maskine ingen unødvendige dele. Dette kræver ikke, at forfatteren kortlægger alle sætninger eller undgår alle detaljer og behandler emner kun i disposition, men at hvert ord fortæller det. [Vægt tilføjet.]
~ William Strunk, Jr., The Elements of Style

ES6 blev standardiseret i 2015, men alligevel i 2017 undgår mange udviklere funktioner såsom kortfattede pilefunktioner, implicitte tilbagevendelses-, hvile- og spredningsoperatører osv.… I form af at skrive kode, der er lettere at læse, fordi det er mere kendt. Det er en stor fejltagelse. Kendskab kommer med praksis, og med fortrolighed er de kortfattede funktioner i ES6 helt klart overlegne ES5-alternativer: kortfattet kode er enkel sammenlignet med det syntaks-tunge alternativ.

Kode skal være enkel, ikke forenklet.

I betragtning af at den kortfattede kode er:

  • Mindre bug tilbøjelig
  • Nemmere at fejlsøge

Og i betragtning af at bugs:

  • Er ekstremt dyre at ordne
  • Tendens til at forårsage flere fejl
  • Afbryd strømmen af ​​normal funktionsudvikling

Og da den kortfattede kode også er:

  • Nemmere at skrive
  • Nemmere at læse
  • Nemmere at vedligeholde

Det er værd at træningsinvesteringen for at bringe udviklere op på hastighed ved hjælp af teknikker som kortfattet syntaks, currying og sammensætning. Når vi ikke gør det af hensyn til fortroligheden, taler vi med læserne om vores kode, så de bedre kan forstå det, som en voksen, der taler babysnak med et lille barn.

Antag, at læseren ikke ved noget om implementeringen, men antag ikke, at læseren er dum, eller at læseren ikke kender sproget.

Vær klar, men lad det ikke stumme. At dumme ting ned er både spildende og fornærmende. Foretag investeringen i praksis og fortrolighed for at få et bedre programmeringsordforråd og en mere energisk stil.

Kode skal være enkel, ikke forenklet.

Niveau dine færdigheder op med Live 1: 1-mentorskab

DevAnywhere er den hurtigste måde at niveau op til avancerede JavaScript-færdigheder på:

  • Live lektioner
  • Fleksible timer
  • 1: 1 mentorskab
  • Lav rigtige produktionsapps
https://devanywhere.io/

Eric Elliott er forfatteren af ​​“Programmering JavaScript Applications” (O’Reilly) og medstifter af DevAnywhere.io. Han har bidraget til softwareoplevelser for Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC og topindspilningskunstnere, herunder Usher, Frank Ocean, Metallica og mange flere.

Han arbejder hvor som helst, han vil med den smukkeste kvinde i verden.