JavaScript: Det mest mærkværdige

Jeg er ved at strikke et fordrag sammen om det næsten uudtømmelige emne "JavaScript in Browsers - WTF", som jeg skal holde på JSDay i Italien om halvanden uges tid. Den gode nyhed er dog, at det med de seneste udgave af de mest populære browsere trods alt må være muligt at holde sig indenfor tidsrammen på en halv time; hvis jeg havde skullet holde foredraget, da IE6 var på sit højeste, er jeg ikke sikker på, at tilhørerne havde nået at få frokosten med den dag.

En af mine favorit-mærkværdigheder i JavaScript er eval-funktionen. Der er flere ting, der er sjov ved den, bl.a. at navnet har en helt afgørende betydning:

Hvem får ret her?

var mening = "Gnavensmølf: Jeg hader JavaScript";
 
function hvadSynesDu() {
     var mening = "Pyntsmølf: JavaScript er så smukt";
     eval("alert(mening);");
}
hvadSynesDu();

Eller her?

var mening = "Gnavensmølf: Jeg hader JavaScript";
var f = eval;
 
function hvadSynesDu() {
     var mening = "Pyntsmølf: JavaScript er så smukt";
     f("alert(mening);");
}
hvadSynesDu();

Overraskende nok får man forskelligt resultat i det første og andet eksempel, selvom eneste forskel er, at man i det andet eksempel giver eval-funktionen et alias (f). Når eval-funktionen tildeles et alias, ændrer det på hvilket scope, den evaluerer i. I første eksempel evalueres i local scope (og det bliver derfor pyntesmølfs mening, vi får at høre), mens det i andet eksempel evalueres i global scope (og det bliver derfor gnavensmølf, vi hører fra).

Hvad så med setTimeout? Her kan man også nemt blive snydt af scope.

var mening = "Gnavensmølf: Jeg hader JavaScript";
 
function hvadSynesDu() {
     var mening = "Pyntsmølf: JavaScript er så smukt";
     setTimeout("alert(mening);", 100);
}
hvadSynesDu();

er ikke helt det samme som

var mening = "Gnavensmølf: Jeg hader JavaScript";
 
function hvadSynesDu() {
     var mening = "Pyntsmølf: JavaScript er så smukt";
     setTimeout(function () { alert(mening);}, 100);
}
hvadSynesDu();

I første tilfælde vil vi høre fra gnavensmølf, fordi den streng, der er givet som første argument til setTimeout, evalueres i global scope. I det andet tilfælde, hvor vi har givet en funktion som argument, vil "mening"-variablen referere til den lokalt definerede, og vi får derfor pyntesmølfs mening at høre.

Det er sådan noget, der gør JavaScript rigtig svært at læse nogle gange; små detaljer kan have stor betydning.

Hvad er det mærkeligste, du har oplevet med JavaScript?

Kommentarer (16)
sortSortér kommentarer
  • Ældste først
  • Nyeste først
  • Bedste først
Jesper Kristensen

Jeg synes faktisk ikke dit eksempel er så interessant. Eval er noget skidt (både funktionen og operatoren) og det eneste man skal vide om den er at man aldrig skal bruge den, og hvis man ser noget kode der bruger den skal man skrive den om. Hvis man skal eksekvere en tekststreng som JavaScript, kan man bruge Function-constructoren.

Hvis man bruger "eval" eller "with" stopper browseren al JIT-optiomering, så det er relativt nemt at huske, at man bare skal lade være med at bruge dem.

Det er langt "sjovere" når det kommer til mærkværdigheder, som man faktisk kan støde på i reel kode.

Her er et af mine yndlings-eksempler:

<script>var open = "hello"</script>  
<span id="x">A</span>  
<script>document.getElementById('x').onclick = function() { alert(open); };</script>  
<span onclick="alert(open)">B</span>  
<span onclick="alert(window.open)">C</span>

Her overskriver vi browserens indbyggede window.open()-funktion, og prøver at læse værdien af den på tre forskellige måder. Hvis man klikker på A får man som forventet den overskrevne værdi, men hvis man klikker på B, får man på mystisk vis den oprindelige værdi. Et klik på C giver igen den overskrevne værdi. Det kan være en udfordring når en browser begynder at implementere en ny standard, som tilfældigvis bruger samme navn som en af dine globale variable.

http://wtfjs.com/ har også en lang række eksempler.

  • 3
  • 0
Jacob Christian Munch-Andersen

Jeg opdagede at det in ny version af Node.js er hurtigere at benytte capturing groups i regular expressions end at benytte non-capturing groups. Gennemgående virker det som at det er hastighedsoptimeringerne der virkelig kan rykke JavaScript på WTF fronten i øjeblikket. Sproget er så gennemgående ukompilerbart at der sker rigtigt mange sjove ting når man alligevel forsøger at kompilere det i en eller anden grad.

Ellers er jeg vist efterhånden så vant til JavaScript at sproget normalt virker ganske logisk.

Dit første eksempel virker i øvrigt ikke som det ser ud til, prøv at køre:

var mening = "Gnavensmølf: Jeg hader JavaScript";  
   
function hvadSynesDu() {  
     var mening = "Pyntsmølf: JavaScript er så smukt";  
     (1,eval)("alert(mening);");  
}  
hvadSynesDu();

Et meget grimt hack i ES5: http://perfectionkills.com/global-eval-what-are-the-options/

  • 0
  • 1
Jacob Christian Munch-Andersen

Hvis man bruger "eval" eller "with" stopper browseren al JIT-optiomering, så det er relativt nemt at huske, at man bare skal lade være med at bruge dem.


Gammel skrøne, jeg har lige prøvet med både Chrome og IE9 og ingen af dem giver bare en antydning af ændret performance ved at evale og withe et stykke ikke-performance-kritisk kode. Dertil er Chrome også fuldstændig ligeglad med at performance kritisk kode er defineret inde i en eval, jeg kan lave hele bunken af JavaScript om til en streng og evale den med et enkelt kald, det gør ikke nogen synlig forskel.

  • 1
  • 0
Anne-Sofie Nielsen

Dit første eksempel virker i øvrigt ikke som det ser ud til

Hm, nu har jeg lige prøvet igen og det ser da ud til at virke fint - har prøvet i Chrome og IE, da jeg ikke lige har Firefox installeret på den computer, jeg sidder ved her.

Der er det pyntesmølfen, der får lov at sige noget, altså lokalt scope.
I dit eksempel - som er en anden måde at lave indirekte kald af eval end at lave et alias - er det det globale scope, man rammer, som der jo også står i artiklen. Og det synes jeg godt nok er forvirrende!

  • 1
  • 0
Jesper Kristensen

Ok, jeg kan godt se det var upræcist hvad jeg skrev. Browseren kan stadig foretage mange optimeringer, men der er nogen den ikke kan bruge, når man bruger direct eval eller with. Her er en simpel kode med en memory leak. Men den er noget større med direct eval i forhold til indirect eval (testet i Firefox og Chrome).

<div id="counter"></div>  
<script>  
var counter = document.getElementById("counter");  
var count = 0;  
var list = [];  
setInterval(function() {  
    var a = [];  
    for (var i = 0; i < 1000000; i++)  
        a.push(i);  
    count++;  
    counter.textContent = count;  
    list.push(function() {  
        var b = eval("1+1");  
        //var b = (1,eval)("1+1");  
    });  
}, 0);  
</script>
  • 1
  • 0
Jacob Christian Munch-Andersen

Virkningen er som du skriver, din kode kode ser blot ud til at være bygget på troen om en anden (og langt mere logisk) sammenhæng.

@Jesper Kristensen - Re: Andre mærkværdigheder
Det memory leak har ikke så meget med eval at gøre, eval er blot det element som i dette tilfælde forhindrer garbage collectoren i at fjerne a i den ydre funktion. Du kunne også have skrevet:

<div id="counter"></div>    
<script>    
var counter = document.getElementById("counter");    
var count = 0;    
var list = [];    
setInterval(function() {    
    var a = [];    
    for (var i = 0; i < 1000000; i++)    
        a.push(i);    
    count++;    
    counter.textContent = count;    
    list.push(function() {    
        var b = a;  
    });    
}, 0);    
</script>

Hvilken giver et tilsvarende møster helt uden brug af eval.

  • 1
  • 0
Anne-Sofie Nielsen

Jacob skrev:

Hvilken giver et tilsvarende møster helt uden brug af eval.

Ja, men jeg synes da nok, at det i Jespers eksempel er sværere at gennemskue, at man har et memory leak, fordi a ikke kan garbage collectes. I eksemplet uden eval er det lidt mere lige til at se, at man holder en reference til a.

IE8 og tidligere havde i øvrigt et endnu mere uigennemskueligt memory leak-problem i forbindelse med XMLHttpRequests, men det har de da heldigvis fikset i 9'eren.

Fald for resten over et interessant skriv om typiske memory leaks i JavaScript her: http://javascript.info/tutorial/memory-leaks

  • 0
  • 0
Anne-Sofie Nielsen

Ifølge det link om JS memory leaks, som jeg skrev om lige før, så behøvede man ikke engang at referere "a" i Jacobs eksempel; der ville alligevel være et memory leak:

JavaScript interpreter has no idea which variables may be required by the inner function, so it keeps everything. In every outer LexicalEnvironment. I hope, newer interpreters try to optimize it, but not sure about their success.

Jeg har dog ikke efterprøvet det; det var måske værd at gøre, da siden er fra 2011.

  • 0
  • 0
Allan Ebdrup

Sjovt eksempel du kommer med. Det er rimelige underlig opførsel fra JS-side.

Jeg har helt instinktivt holdt mig langs væk fra eval, og at kalde setTimeout med en streng, siden jeg begyndte med store JavaScript apps i 2002, altså lang tid før JavaScript: the good parts bogen kom i 2008 http://shop.oreilly.com/product/9780596517748.do og lang tid før JSLint og JSHint. Man roder bare ikke rundt med sin kode som en streng, og slet ikke noget kode der kalder ting fra det omkringliggende scope. Medmindre der virkelig ikke er andre måder at gøre det på. Og det er der som regel.

Der har været nogle få ting der har irriteret mig ved JS, den der bugger mig mest i praksis er nok den måde NaN fungerer. Der kan du finde nogle eksempler på mærkelighed, som folk rent faktisk vil støde på i deres hverdag med JS.

  • 1
  • 0
Anne-Sofie Nielsen

Jesper Kristensen skrev:

<div class="geshifilter"><pre class="html4strict geshifilter-html4strict" style="font-family:monospace;"><span style="color: #009900;"><<a href="http://december.com/html/4/element/script.html"><span style="color: #000000; font-weight: bold;">script</span></a>></span>var open = "hello"<span style="color: #009900;"><<span style="color: #66cc66;">/</span><a href="http://december.com/html/4/element/script.html"><span style="color: #000000; font-weight: bold;">script</span></a>></span>
<span style="color: #009900;"><<a href="http://december.com/html/4/element/span.html"><span style="color: #000000; font-weight: bold;">span</span></a> <span style="color: #000066;">id</span><span style="color: #66cc66;">=</span><span style="color: #ff0000;">"x"</span>></span>A<span style="color: #009900;"><<span style="color: #66cc66;">/</span><a href="http://december.com/html/4/element/span.html"><span style="color: #000000; font-weight: bold;">span</span></a>></span>
<span style="color: #009900;"><<a href="http://december.com/html/4/element/script.html"><span style="color: #000000; font-weight: bold;">script</span></a>></span>document.getElementById('x').onclick = function() { alert(open); };<span style="color: #009900;"><<span style="color: #66cc66;">/</span><a href="http://december.com/html/4/element/script.html"><span style="color: #000000; font-weight: bold;">script</span></a>></span>
<span style="color: #009900;"><<a href="http://december.com/html/4/element/span.html"><span style="color: #000000; font-weight: bold;">span</span></a> <span style="color: #000066;">onclick</span><span style="color: #66cc66;">=</span><span style="color: #ff0000;">"alert(open)"</span>></span>B<span style="color: #009900;"><<span style="color: #66cc66;">/</span><a href="http://december.com/html/4/element/span.html"><span style="color: #000000; font-weight: bold;">span</span></a>></span>
<span style="color: #009900;"><<a href="http://december.com/html/4/element/span.html"><span style="color: #000000; font-weight: bold;">span</span></a> <span style="color: #000066;">onclick</span><span style="color: #66cc66;">=</span><span style="color: #ff0000;">"alert(window.open)"</span>></span>C<span style="color: #009900;"><<span style="color: #66cc66;">/</span><a href="http://december.com/html/4/element/span.html"><span style="color: #000000; font-weight: bold;">span</span></a>></span></pre></div>
Her overskriver vi browserens indbyggede window.open()-funktion, og prøver at læse værdien af den på tre forskellige måder. Hvis man klikker på A får man som forventet den overskrevne værdi, men hvis man klikker på B, får man på mystisk vis den oprindelige værdi.

Faktisk er det ikke den oprindelige værdi, altså window.open, man får, men document.open-funktionen, hvilket er noget helt andet.
Det skyldes, at for en inline event handler er document en del af scope chain.

  • 0
  • 0
Anne-Sofie Nielsen

Der har været nogle få ting der har irriteret mig ved JS, den der bugger mig mest i praksis er nok den måde NaN fungerer. Der kan du finde nogle eksempler på mærkelighed, som folk rent faktisk vil støde på i deres hverdag med JS.

Der er selvfølgelig de klassiske "WTF" med at

typeof(NaN) == "number"

og

NaN != NaN

og så at NaN dukker op på tidspunkter, hvor man mindst venter det, fordi JavaScript i mange tilfælde er "venligt" og ikke kaster exceptions:

var d = new Date("vrøvl");  
alert(d.getMonth());

hvor det, man får ud af at kalde d.getMonth() på en date, der er konstrueret med en ugyldig værdi, bliver NaN.

  • 1
  • 0
Peter Mogensen

Hehe ... jae...
Men uden at jeg på nogen måde er ekspert i hverken JS eller Node.js, så er mit indtryk at det primært skyldes at event-baseret eksekvering er default og alt kode inkl. libraries dermed er gennemsyret af det.
Det giver det nogle fordele fremfor f.eks. Perl, hvor mange libraries slet ikke spiller sammen med en event-baseret model og du derfor afskriver dig en masse standard-libraries, hvis du ønsker det.
De fleste script-sprog (inkl. Perl) har nu nogen gange nogle mærkværdigheder, der gør man får helt lyst til at kode tingene om i C.

  • 3
  • 0
Log ind eller Opret konto for at kommentere