Hoppa till innehållet

JavaScript

språket som webbläsare kör

Vad det är

JavaScript är ett dynamiskt typat programspråk, standardiserat som ECMAScript, som varje webbläsare kan köra och som också körs på servrar och byggmaskiner genom körmiljöer som Node.js. Det kortas ofta till JS. Standardens årliga utgåvor heter ES2015, ES2024 och så vidare, den äldre numreringen (ES5, ES6) hörs fortfarande, och ”vanilla JavaScript” betyder språket utan något ramverk ovanpå. Utöver namnet och en del syntax med klammerparenteser har det lite gemensamt med Java.

Brendan Eich skrev den första versionen på Netscape, och den levererades i webbläsaren Navigator 2.0. Ecma International antog den första utgåvan av standarden, ECMA-262, i juni 1997, och dess kommitté TC39 har gett ut en ny utgåva varje år sedan ECMAScript 2016. Ett förslag kommer in i specifikationen först i steg 4 av kommitténs process, som kräver implementationer som redan har levererats, så texten och motorerna är sällan oense länge.

Språket säger väldigt lite om omvärlden. ECMA-262 definierar syntaxen, typerna, objekten, löftena och modulerna, och lämnar in- och utdata åt den miljö som kör språket. I en webbläsare är den miljön webbplattformen: dokumentet, händelserna, timrarna och nätverksbegärandena, specificerade till största delen av HTML-standarden och dess systerstandarder. I Node.js står miljön i stället för filer, socketar och processer. Den uppdelningen förklarar en vanlig överraskning: kod kan vara fullt giltig JavaScript och ändå gå sönder när den flyttas från en sida till en server, eftersom språket följde med och miljön stannade kvar.

I en webbläsare delar en sidas skript en enda huvudtråd med layout, ritning och inmatning. En uppgift körs till slut innan webbläsaren kan svara på nästa klick. Löften och async-funktioner håller väntandet borta från den tråden, medan beräkningar stannar på den om de inte flyttas till en Web Worker. Tal är flyttal med dubbel precision enligt IEEE 754, så heltal är exakta bara upp till 2^53 − 1, och allt större än så kräver BigInt.

Varför det spelar roll

Det verkliga beslutet är hur mycket av det du skickar till dem som läser webbplatsen. JavaScript är språket som får en sida att bete sig utan en tur till en server, och varje kilobyte av det laddas ned, tolkas, kompileras och körs på en enhet som du inte styr över. Paketet som känns omedelbart på utvecklarens bärbara dator kan hålla en mellanklasstelefon upptagen i flera sekunder, och läsaren betalar med batteri och tålamod.

Skript är också där en sida är mest utsatt. Allt som injiceras där körs med sidans fulla befogenheter: det kan läsa vad läsaren skriver och skicka det vart som helst som sidan får skicka. Det är den risk som Content Security Policy finns till för att stänga in, och en sida utan skript erbjuder ingenting att injicera i. Paketekosystemet lägger till en andra exponering. Ett typiskt bygge av en frontend drar in hundratals transitiva beroenden, och vart och ett av dem är kod som körs med samma rättigheter som din.

Den andra sidan av vågskålen är verklig. Med ett språk på båda sidor av ett system kan webbläsaren och servern dela en valideringsregel eller en dataform, och vissa produkter behöver mycket skript: tillstånd offline, dra och släpp, kartor, samarbete i realtid. Den användbara frågan ställs per funktion: måste det här ske i läsarens webbläsare utan att fråga en server? Om inte, rendera det på servern eller vid byggtid och skicka HTML.

Det närliggande valet är JavaScript eller TypeScript. TypeScript lägger till en typkontroll och ett byggsteg och kompileras tillbaka till vanlig JavaScript. För en kodbas som flera personer ändrar varje vecka brukar typkontrollen betala sig; för en förbättring på hundra rader på en statisk sida räcker JavaScript och en strikt linter.

Så ser det ut i praktiken

Den här webbplatsen skickar nästan ingen JavaScript till sina läsare och kör en hel del av det vid byggtid. Tre sidor visar båda halvorna av det.

Den publika webbplatsen är ett statiskt bygge på tre språk utan backend vid körning, och webbläsaren tar emot ett enda förstapartsskript. Det skriptet är ett avgränsat undantag för en enda funktion, ett portföljfilter vars reglage är dolda tills skriptet har körts. Resten av webbplatsen, 108 mallfiler varav 53 är partials och 18 är shortcodes, gör sitt arbete när webbplatsen byggs. Sidan redovisar också kostnaden för det valet: allt interaktivt måste lösas vid byggtid, och därför kostnadsberäknades ett sökindex och parkerades i stället för att levereras.

Samma innehåll publiceras över Gemini och Gopher, två protokoll som inte har någon plats alls för ett skript. Bygget producerar 777 Gemini-dokument och 777 Gopher-menyer ur samma källa, och en mätning bekräftade att HTML-koden inte ändrades med en enda byte. De två alternativa representationerna lägger till ungefär 12 % till HTML-kodens vikt på disk. Webbplatsen hävdar att ett dokument inte behöver en megabyte skript för att kunna läsas, och ett format som inte kan bära ett enda är det tydligaste sättet att visa det.

Det mesta av JavaScript-koden här bor i verktygen. Webbplatsens kvalitetsgrind består av trettiotvå skript och 10 242 rader JavaScript, den största mängden kod i repot, och under en tid var det den enda kod som ingenting formaterade, lintade eller typkontrollerade. Arbetet med att få den under en formaterare och en linter började med att anpassa formateraren efter koden: repots standard på fyra mellanslag skulle ha skrivit om 2 173 rader i det största kontrollprogrammet, och en åsidosättning till två mellanslag för de filerna lämnade 38 rader verklig avvikelse. Lintern gav sedan tretton fynd, och alla tretton åtgärdades utan att något undertrycktes. Ett fel i ett kontrollprogram visar sig som en kontroll som går igenom när den borde fallera, så grindens egen kod får samma behandling som den ger allt annat.

Var det går fel

Felen nedan följer av hur språket är byggt, och vart och ett har en känd åtgärd.

Sidan ritas upp och ignorerar sedan tryck i en sekund eller mer. En lång uppgift, som att hydrera ett stort paket, tolka ett stort JSON-svar eller sortera tusentals rader, håller den enda tråd som hanterar inmatning. Skicka mindre kod för den första interaktionen genom att dela upp paketen per rutt och skjuta upp resten, bryt upp långt arbete i bitar som lämnar tillbaka kontrollen till händelseloopen, och flytta tunga beräkningar till en Web Worker. Mät på en långsam telefon i stället för på den bärbara dator som skrev koden.

Priser och identifierare blir en aning fel. Varje tal är ett binärt flyttal, så decimala bråk avrundas och mycket stora heltal faller ihop med sina grannar. En 64-bitars databasnyckel som skickas som ett tal i JSON är det vanliga offret:

0.1 + 0.2 === 0.3; // false
Number.MAX_SAFE_INTEGER; // 9007199254740991
9007199254740993 === 9007199254740992; // true

Håll pengar i heltal av den minsta enheten, t.ex. ören eller cent, skicka stora identifierare som strängar och använd BigInt där aritmetiken måste vara exakt.

En jämförelse släpper igenom fel värde. Operatorn == konverterar sina operander innan den jämför dem, så både "" == 0 och "0" == false är sanna. Använd === överallt och låt linterregeln eqeqeq upprätthålla det, så att konverteringen blir något som koden ber om öppet.

Text från en användare körs till slut som kod. Att tilldela opålitlig text till innerHTML, bygga kod genom att foga ihop strängar eller lämna indata till eval gör data till skript. Skriv text med textContent, skicka en Content Security Policy som bara släpper in skript via nonce eller hash, och håll listan över beroenden kort och låst i en låsfil, och läs vad varje uppdatering drar in.

Kod som fungerar i webbläsaren kraschar på servern. Språket flyttade och miljön gjorde det inte: window och document finns bara i en webbläsare, och Node.js läser en fil som CommonJS eller som en ES-modul beroende på inställningar som webbläsaren aldrig ser. Håll miljöspecifika anrop i kanterna, sätt "type": "module" uttryckligen i package.json och kör varje del av koden i den miljö där den faktiskt ska köras.

Läs mer

Ordlista