JS og regulære udtryk - do you feel lucky, punk?
Hvis man skal skrive regulære udtryk i JavaScript, så er det måske meget godt at starte på en dag, hvor solen skinner, da både JavaScript og regulære udtryk er noget, folk har det med at falde og slå sig på. Den gode nyhed er dog, at der ikke er det helt store forskelle mellem fortolkningen af regulære udtryk på tværs af browserne - de har alle lagt sig pænt op ad ECMA-standarden.
Internet Explorer-teamet har f.eks. i senere versioner fået rettet op på den lille mærkværdighed, at IE8 tillod, at punktum matchede “carriage return”-tegnet. Men der er stadig visse forskelle mellem browserne, se nu eksemplet nedenfor:
var text = "Jeg elsker v2.dk"; var regex = /Jeg elsker v(ersion)?2.dk/; text.replace(regex, function(grp0, grp1) { alert(grp1); } );
Den funktion, der gives som andet argument til replace(...) kaldes for hvert match af det regulære udtryk.
Spørgsmål: Hvilken værdi får parametren grp1 i den anonyme funktion ovenstående eksempel?
Svar: Det kommer an på browseren.
- I IE og Chrome er værdien undefined.
- I Firefox er værdien en tom streng. Det virker umiddelbart ikke som en særlig praktisk valg - det er rart at kunne skelne mellem, at gruppen matchede ingenting eller slet ikke blev matchet.
Til gengæld kan man godt få sig nogle overraskelser, hvis man er vant til at skrive regulære udtryk i sprog som Java eller C#, eller f.eks. får den idé at kopiere et regulært udtryk fra et sprog til et andet for at lave samme validering på backend og frontend (med mindre man har fornøjelsen af at bruge et framework som node.js, hvor man kan hygge sig med JavaScript på begge sider).
JavaScript er jo i mange sammenhænge kendt som et ganske tilgivende sprog, hvilket også viser sig i implementeringen af regulære udtryk. Har man eksempelvis fået lavet en ugyldig/unødvendig escaping af et tegn, vil JavaScript konvertere dette til hvad man sandsynligvis mente.
Spørgsmål:
F.eks. findes der ikke nogen escape-sekvens \a. Så hvad betyder det, hvis programmøren har skrevet:
Java:
Pattern.compile("\\a")
JavaScript:
new RegExp("\\a")
Svar:
Ovenstående Java-kode vil resultere i en syntaks-fejl på kørselstidspunktet - når der ikke findes en \a, så skal man lade være med at skrive det!
I JavaScript får man derimod et regulært udtryk, som ganske enkelt matcher tegnet “a” - mon ikke det var det, man mente?
Tilsvarende vil JavaScript også oversætte forkerte Unicode-escape-sekvenser som f.eks. /\united/ til at opfatte \u som u, altså /\united/ matcher det samme som /united/, også selvom JavaScript regulære udtryk understøtter \u-syntaksen for Unicode, eks. \u0061 for tegnet “a”. Den var heller ikke blevet accepteret i Java.
Nå, videre til sjov med back references. :-) Her har jeg puttet det samme regulære udtryk ind i Java og JavaScript:
Java:
Pattern.compile("The so-called (')?internet\\1")
JavaScript:
new RegExp("The so-called (')?internet\\1")
Bemærk, at udtrykket slutter med en back-reference til gruppe nr. 1, altså at den sidste del af udtrykket skal være identisk med det, som første parentesudtryk matchede.
Spørgsmål: Hvilke af nedenstående strenge vil ovenstående matche?
1) "The so-called ‘internet’"
2) "The so-called internet"
Svar:
1) Matcher både i Java og JavaScript.
2) Matcher kun i JavaScript. Og hvorfor? Gruppe nr. 1 - der kunne have indholdt en apostrof, men ikke gjorde det i dette tilfælde - bliver slet ikke matchet. JavaScript lader så \1 matche den tomme streng, mens Java ikke lader \1 matche noget som helst. Altså matcher hele udtrykket kun strengen “The so-called internet” i JavaScript og ikke i Java.
Inden I er blevet helt skæve i hovedet, så lad os kigge på et sidste regulært udtryk:
Java:
Pattern.compile("V(ersion)?2-blogger: Jeg elsker v\\12")
JavaScript:
new RegExp("V(ersion)?2-blogger: Jeg elsker v\\12")
Spørgsmål: Matcher de strengen "Version2-blogger: Jeg elsker version2"?
Svar: Det matcher i Java, men ikke i JavaScript. JavaScript opfatter sidste del af udtrykket, nemlig \12 som en reference til gruppe nr. 12 - og den findes jo ikke i dette eksempel. Java lader til at have en mere flydende fortolkning: Fordi der ikke findes en 12. gruppe i udtrykket, opfattes \12 som gruppe 1 efterfulgt af et 2-tal. Altså vil udtrykket matche "Version2-blogger: Jeg elsker version2" i Java.
Og så kan man i øvrigt også komme i problemer, hvis man forsøger at kopiere et udtryk, der benytter sig af features, som ikke eksisterer i alle varianter af regulære udtryk, eksempelvis possessive quantifiers, der kan være en smart måde at vride bedre performance ud af visse regulære udtryk på i Java, men som ikke understøttes i JavaScript (eller C# for den sags skyld).
Så pas på - regulære udtryk opfører sig ikke nødvendigvis ens på tværs af sprog eller browsere. Og irriterende nok viser en del af forskellene sig måske ikke ved første afprøvning, men kun i specialtilfældene. Suk!
