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!

Kommentarer (13)
sortSortér kommentarer
  • Ældste først
  • Nyeste først
  • Bedste først
Martin Kofoed

Javascript kan helt klart øge wtf/min ratio'en på mange projekter. F.eks. sjov med numre:

(42).toFixed(2);  // "42.00"  
42.toFixed(2);  // syntax error  
42.888.toFixed(2);  // "42.89"  
42..toFixed(2);  // "42.00"  
42...toFixed(2);  // TypeError

Eksemplet er det jeg lige kan huske fra wtfjs. Der er en perlerække af gode eksempler her:

http://wtfjs.com/

Hvilke tools anvender I derude til test af små JavaScript-snippets? Jeg bruger Chromes js console. Ellers er der Rhino til en linux kommandolinie og jsc til Mac ditto.

  • 3
  • 0
Torben Mogensen Blogger

... skrive "The so-called ('?)internet\1" i stedet for "The so-called (')?internet\1"? Så vil ('?) jo altid matche et eller andet (den tomme tegnfølge eller et anførselstegn).

Generelt vil jeg foretrække exceptions eller "undefined", hvis et udtryk ikke matcher, og jeg er heller ikke gode venner med JavaScripts tendens til at gætte, hvad du mener, hvis det, du skriver, ikke giver mening.

  • 2
  • 0
Anne-Sofie Nielsen

... skrive "The so-called ('?)internet\1" i stedet for "The so-called (')?internet\1"? Så vil ('?) jo altid matche et eller andet (den tomme tegnfølge eller et anførselstegn).

Jo, det burde man. Men med dette blogindlæg ville jeg gerne fortælle skrækhistorien om, hvad der sker, hvis man ikke gør det ;-)

  • 2
  • 0
Torben Mogensen Blogger

Jo, det burde man. Men med dette blogindlæg ville jeg gerne fortælle skrækhistorien om, hvad der sker, hvis man ikke gør det ;-)

Hvilket understøtter pointen om, at det er bedre at få en fejlmeddelelse i stedet for en tom tegnfølge -- hvis det "ser ud til at virke", finder du ikke eventuelle fejl, som du ville finde, hvis du fik en fejlmeddelelse.

  • 2
  • 0
Rasmus Christensen

Kan helt klart se fordelen i at skrive regex udtryk, men ikke nødvendig som første løsning på en problemstilling. Selv i C# som er mit primære sprog er det ikke regex udtryk jeg ser flest af når vi har review.
Tit er det resultatet af et review eller blot et ønske om at optimere kode der skal matche x :)
Uanset om det er javascript, C# eller? så ligger det helt klart op til at man skriver et par test, som sikkert også vil være med til at forklare hvad udtrykket egentlig gør. Det vil også gøre det nemt at teste for hvordan udtrykket virker i forskellige browserer.

  • 0
  • 0
Peter Makholm

Uanset om det er javascript, C# eller? så ligger det helt klart op til at man skriver et par test, som sikkert også vil være med til at forklare hvad udtrykket egentlig gør.

Den sætning gør mig lidt målløs. Hvis der er behov for at skrive et par test for at forklare hvad udtrykket gør så skriver man for komplicerede regulære udtryk i forhold til hvor godt man har styr på regulære udtryk. (Og 'man' dækker her over hele ens team).

Selvfølgelig er der behov for at eksperimentere når man bliver præsenteret for en ny dialekt og for at blive bedre til at bruge værktøjet. Men i min daglige kode finder jeg lige så lidt behov for specifikt at skulle teste om jeg forstår regulære udtryk som jeg finder behov for at teste om jeg forstår sin() eller unlink() funktionerne.

Jeg anser mig selv for at være meget godt inde i den RegExp-dialekt jeg anvender til dagligt og jeg skriver mange regexpes. Men det er nu også sjældent at de er mere kompliceret end dem Anne-Sofie bruger i sit indlæg.

Det vil også gøre det nemt at teste for hvordan udtrykket virker i forskellige browserer.


... men det forklare selvfølgelig noget når man slet ikke har styr på hvilket regexp-dialekt man skriver til.

  • 0
  • 0
Peter Makholm

Angående /(')?internet\1/:

Generelt vil jeg foretrække exceptions eller "undefined", hvis et udtryk ikke matcher, og jeg er heller ikke gode venner med JavaScripts tendens til at gætte, hvad du mener, hvis det, du skriver, ikke giver mening.

Forskellen mellem Java og JavaScript kan vel forklares i deres type-systemer. I JavaScript er den primitive type nullable og så er det vel en rimelig antagelse at NULL-værdien i en regexp er en zero width assertion der matcher alle steder.

Mere interessant er det måske at sammenligne med Perl, der har et tilsvarende typesystem. I Perl matcher udtrykket kun første streng, ligesom Java.

Dokumentationen for Perls regulære udtryk siger:

Groups are numbered with the leftmost open parenthesis being number 1, etc. If a group did not match, the associated backreference won't match either. (This can happen if the group is optional, or in a different branch of an alternation.)

Det giver for mig også fint mening.

Jeg tror at forskellen er om man opfatter \1 som en variable som ved en non-match får tildelt NULL-værdien eller mere direkte henviser til beregningen af NFA/DFA'en. Når jeg tænker dybt over det, så giver Perl og Java mest mening for mig.

... men jeg må tilstå at jeg skulle eksperimentere og nærlæse dokumentation. Jeg bruger meget sjældent backreferencer og specielt ikek hvor de er valgfrie. Så den egentlige feature falder i 'for kompliceret til min normale brug' kategori og som trykfejl falder den i samme kategori som at have glemt et fortegn i en beregning, hvilket jeg heller ikke altid eksplicit ville dokumentere mig udad.

  • 0
  • 0
Log ind eller Opret konto for at kommentere
IT Company Rank
maximize minimize