64 KiB
+++ title = "Introduction to JavaScript" subtitle = "Writing JS, Pre-ES5 era." archived = true draft = false tags = [ "javascript" ] date = "2013-10-01" categories = ["programming"] +++
Javascript Inleiding
Primitives
Soorten
In javascript zijn er slechts 3 primitives:
string
(geassocieerd object:String
)boolean
(geassocieerd object:Boolean
)number
(geassocieerd object:Number
)
Primitives zijn immutable! Toekennen van properties maakt ook een tijdelijk object aan, dus dit heeft geen nut.
Soort afchecken
Aangezien JS loosely typed is, kunnen we nooit weten wat er nu in var variabele;
steekt op een bepaald moment in de code.
Om dit op te lossen kan men typeof
gebruiken, een functie die een string teruggeeft wat het type van die variabele is.
Typeof retourneert in het geval van een object, de string object
, in alle andere gevallen de bovenstaande primitive namen.
Object coercing
-> (Lees eerst het stuk over objecten etc aub!)
Elk van de primitives worden door Javascript automatisch geconverteerd naar hun object representatie wanneer men properties of functies hierop probeert toe te passen. Bvb:
var someString = "someString";
someString.indexOf("String"); // indexOf() wordt op new String("someString") opgeroepen
Direct nadat dit geëvalueerd wordt schiet de garbage collector aan de gang en wordt het tijdelijk object verwijderd.
var tekst = "dit is tekst jong";
tekst.length = 3; // -> String object created & garbage-collected
console.log(tekst); // dit is tekst jong (old primitive value)
Het object dat wordt aangemaakt bijhouden en daar de lengte van afkappen is zelfs gevaarlijk:
var s = new String("ss");
s.length = 1;
for(var i = 0; i < s.length; i++) {
console.log(s[i]); // prints only once 's'
}
console.log(s); // prints 'ss'??
Als we weten dat een object aangemaakt wordt zodra we een property oproepen, vraagt een mens zich af, hoe zit dat met number
s? De .
accessor wordt hier gebruikt om komma's voor te stellen... Wel, aangezien elk object ook values van properties kan retourneren via de []
operator (zie later), werkt dit dus wel:
21.54["toFixed"]() // returns 22, way cool!
Zie Mozilla MDC Docs: JS Reference: Number voor wat mogelijke functies zijn op Number
.
Objecten en Functies
Douglas Crockford:
An object is a dynamic collection of properties.
As opposed to "instances of classes".
Een functie in JS kan net zoals andere waarden toegekend worden aan een variabele. Een functie ís in feite een waarde. Dit is anders in bijvoorbeeld Java, een "functie" (method) is een syntax block code die uitgevoerd kan worden. Wanneer een functie aan een property van een object gekoppeld is spreken we van een method op dat object.
Een object bevat properties. Een functie is een rij van expressies die één waarde retourneert voor dezelfde input te evalueren. Bekijk een "functie" als zijn mathematische beschrijving: f(x)
.
Object Literals
Een object definiëren gaat heel makkelijk:
var Ezel = new Object();
Ezel.poten = 4;
Ezel.balk = function() {
console.log("iaia met mijn " + this.poten + " poten");
};
Zodra een Object
gemaakt wordt, kunnen eender welke property keys toegekend worden met eender welke variabele. Dit kan ook op een associatieve manier, bijvoorbeeld Ezel["poten"] = 4;
.
Bekijk het volgend stukje code:
var Ezel = {
poten: 4,
balk: function() {
console.log("iaia met mijn " + this.poten + " poten");
}
};
Hier wordt een object door middel van een literal gedefiniëerd. De correcte manier om de ezel te laten balken is Ezel.balk()
. Merk op dat Ezel
een gewoon object met wat simpele properties is, bekijk het als een map. Dit is een instantie van Object
: typeof Ezel
retourneert dit.
JSON en Object Literals
JSON ((JavaScript Object Notation)) is een subset van JavaScript, dus door de JS Compiler op te roepen met eval()
is het heel eenvoudig om JSON Strings te evalueren. Een JSON Object is in feite een JS Object Literal:
var JSONObj = {
"bindings": [
{ "key": "value1", "key2": "value2" },
{ "key": "value3", "key2": "value4" }
]
}
Dat object kan via een string opgebouwd worden door eval()
te gebruiken, maar dit is gevaarlijk aangezien van een externe webserver ook JS functies in dat data object kunnen zitten! Gebruik daarvoor altijd een JSON Parser, zoals de built-in JSON.parse()
.
Om de omgekeerde weg uit te gaan, van object naar string, gebruik JSON.stringify()
. Meer uitleg, zie bovenstaande link.
Functie Literals
Een functie definiëren gaat op drie manieren:
function bla() { ... }
- de normale manier, een function statement.var bla = function() { ...}
- een function literal, ofwel een lambda function (Lisp referentie). In feite een closure die toevallig toegekend wordt aan variabelebla
.var bla = new Function("", "...")
met als eerste argument de argumenten van de functie, allemaal instring
vorm.
De onderstaande code definiëert bijvoorbeeld in lambda-stijl een functie genaamd Ezel:
var Ezel = function() {
this.poten = 4;
this.balk = function() {
console.log("iaia met mijn " + this.poten + " poten");
}
};
Merk de verschillen op met het bovenstaande:
function()
staat voor{
.this
wordt gebruikt om keys aan te spreken! Waarom? zie scoping.- De functie bevat gewone statements met
=
en niet met:
. Dit is geen object maar een functie!
!! De functie die net aangemaakt is kan nu als constructor dienen. Die werd intern ook aan de Ezel prototype
property gekoppeld:
Ezel.prototype.constructor
De correcte manier om de ezel te laten balken is new Ezel().balk()
.
Meer info over hoe new
werkt om de ezel instance de poten en de balk functie toe te kennen: zie inheritance.
Functies zijn objecten
Functions are first class Objects!
Bewijs: (Uitleg: zie inheritance)
Function.prototype.__proto__ == Object.prototype;
(function fn(){}).__proto__ == Function.prototype;
Dus functies zijn ook associatieve maps, dus (function(){}).bla = 3;
werkt perfect! Zie scoping, deel "Object/Class Member variables".
De kracht van closures
In Javascript is élke functie een closure! In sommige andere talen kan enkel een closure "deep bindings" uitvoeren (= scope chain bijhouden, zie scoping deel), of een functie opnieuw retourneren. In JS is er geen verschil tussen een literal function en een gewone, buiten de notatie - een literal bind een anonieme functie aan een variabele. Zoiets kan dus perfect:
a = function() {
var aGetal = 10;
return function() {
var bGetal = 20;
return function() {
var cGetal = 30;
return aGetal + bGetal + cGetal;
}
}
}
a()()() // 60
Merk op dat de ()
operator een functie uivoert, en hier alles 3x genest is, maar toch de binnenste functie een referentie naar aGetal
kan gebruiken, gedefiniëert in de buitenste functie! Lees hierover meer in het scoping gedeelte.
Declaratie van functie literals
In javascript worden variabelen die met var
gedeclareerd worden altijd vooraan geplaatst, ook impliciet. Dit wil zeggen dat wanneer ik een function literal definieer, eigenlijk het volgende gebeurt:
function x() {
alert("yo");
var b = function() { };
} // is for the JS parser equal to:
function xParsed() {
var b;
alert("yo");
b = function() { };
}
Wanneer je b()
probeert uit te voeren vóórdat b effectief geïnitialiseerd is, ontploft de boel. Let hier dus op, en gebruik als dit niet mogelijk is geen literal. Bij naamgevingen binnen een scope geldt een volgorde:
- Language-defined (
this
,arguments
) - Formal-defined (arguments)
- Function-defined
- Variable-defined
Dat wil zeggen dat wanneer je twee keer x probeert toe te kennen aan een getal én een functie, heeft de functie voorrang.
!! Uitzondering: een formeel argument met naam arguments
heeft voorrang op lang-defined arguments
. Bad practice...
Itereren over properties van objecten
Vergeet niet dat in Javascript alle objecten (zowel object literals als instanties van functies) zich gedragen als een {{< wp "Associative array" >}} - en dat we dus ook kunnen lopen over alle keys:
var N = { one: 1, two: 2 };
for(key in N) {
console.log(key + " represents as value: " + N[key]);
}
Om de value van een bepaalde key op te halen kan je natuurlijk ook eval('N.' + key)
gebruiken - alle wegen leiden tot Rome...
Controleren of een key in een object/array beschikbaar is
Dit kan op enkele manieren:
'key' in object
((Opgelet:in
kan misleidend effect hebben bij gebruik in combinatie met prototypal inheritance, zie later!))object.hasOwnProperty('key')
-> gaat welfalse
retourneren bij overgenomen keys vanuit prototypes. Zie later.- nogmaals itereren (er zijn betere ideeën, dit is de neiging die veel mensen hebben, te procedureel denken!)
Wat géén goede manier is:
var Obj = {
x: 'x',
u: null,
un: undefined
};
Obj.x === undefined // false, so it must exist as a key, right?
Obj.u == undefined // whoops, true? oplossing: gebruik '#####'
Obj.un == undefined // whoops, still true? value kan ook undefined zijn natuurlijk!
#####= Varargs #####=
function f() {
return arguments[0] + arguments[1];
}
f(1, 2) // == 3
Indien er geen argumenten gespecifiëerd zijn, zijn ze allemaal optioneel! Dit wil zeggen dat een functie aangeroepen kan worden zonder het aantal argumenten 100% te laten overeen komen.
anonymus functions en recursie
arguuments
heeft nog een speciale property: arguments.callee(...)
dat de huidige functie voorstelt, hiermee kan je jezelf aanroepen!
!! Dit gaat een syntax error geven bij ECMA Script standaard 5 in strict mode
verplicht alle argumenten invullen
function f(x, y, z) {
if(arguments.length != arguments.callee.length) {
throw new Error("kapot wabezig");
}
}
f(1, 2) // == 3
Het is natuurlijk makkelijker om != 3
hardcoded te plaatsen, maar dit kan extracted worden naar een aparte functie.
Function overloading
Overloading bestaat niet in JS aangezien de tweede definitie van de functie de eerste overschrijft (de property "functienaam" in het object, zeg maar).
Het is wel mogelijk om één functie te maken die delegeert, zie http://ejohn.org/blog/javascript-method-overloading/
Maak handig gebruik van het feit dat de .length
property ook op functies opgeroepen kunnen worden! (Telt enkel de gedefiniëerde)
(function x(y, z) {}).length ###### 2; // y and z
#####= this Keyword #####=
Zie scoping
#####= Aspect Oriented Programming in JS #####=
Via AOP kan men net voor of na een bepaalde functie gedrag toevoegen zonder dat een component dat die functie aanroept daar weet van heeft. Het is zo makkelijk om bijvoorbeeld logging toe te voegen. In Java wordt Spring AOP gedaan via proxy beans die van dezelfde interface afleiden en daarna delegeren, of via AspectJ die rechtstreeks bytecode wijzigt.
In Javascript kan dat makkelijker, omdat we de referentie naar een functie gewoon kunnen "vast" pakken. Stel dat ik voordat de ezel balkt wil loggen "ik ga balken":
var oldBalkF = Ezel.prototype.balk; // vereist dat balk via `prototype` werd toegevoegd
Ezel.prototype.balk = function() {
console.log("ik ga balken! ");
oldBalkF.call(this);
}
new Ezel().balk(); // print log eerst
Hier merken we twee dingen op:
- In de nieuwe balk functie kan niet zomaar
oldBalkF()
uitgevoerd worden dan is mijnthis
referentie naar de ezel instantie weer weg. - De
window
scope werd vervuild door oldBalkF, die nog steeds toegankelijk is. Hier zijn twee oplossingen voor: 1.delete oldBalkF;
na decall
instructie (extra werk) 1. Gebruik een anonieme functie die direct uitgevoerd wordt die de scope bewaakt!
#####= undefined #####=
Refereren naar een property in een object dat niet bestaat, geeft ons de speciale waarde undefined
.
Merk op dat dit niet hezelfde is als null
. Toch zijn ze gerelateerd:
var x = {
one: 1
};
if(x.two == null) {
3;
}
Dit geeft 3 omdat x.two ##### undefined
en null ###### undefined
. Aangezien null
in een if test by default true
retourneert, kan de if test korter: if(x.two) { ... }
.
Zo is het makkelijk om optionele argumenten van functies na te gaan:
function count(one, two, three) {
if(!two) two = 0;
if(!three) three = 0;
return one + two + three;
}
!! undefined
is een window-scope variabele die initiëel de value undefined
heeft, dit is dus géén keyword, pas op!
Async coding in JS
Asynchronous programming in JS: APIs interview (infoQ)
Het probleem
Alle events in javascript zijn asynchroon. Dat wil zeggen dat we geen idee hebben wanneer de eigenlijke code uitgevoerd is, en we een callback closure moeten meegeven, die verder werkt als de asynchrone code uitgevoerd is.
Dit is oké voor 1-2 asynchrone calls. Maar stel u voor dat we 4+ async calls moeten maken om werk gedaan te krijgen. Typisch dingen zoals:
- setTimeouts
- animaties (jQuery ea)
- AJAX calls (REST, naar domein logica, bewaren, opvragen, veranderen, ...)
Een integratietest schrijven in JS
In Java kunnen we gewoon wat methods oproepen die data persisteert, daarna de eigenlijke asserts schrijven en eventueel in de @After
met JUnit data cleanup uitvoeren:
DomainObj obj = new DomainObjPersister()
.withA()
.withLala("lala")
.persist();
ChildObj child = new ChildObjPersister()
.withParent(obj)
.persist();
assertThat(child.getStuff()).isNotEmpty();
Om child
te kunnen persisteren moeten we obj
als parent meegeven, dus die call moet eerst uitgevoerd zijn. Alle persisters gaan naar de database. Dit zou in javascript zoiets zijn=
$.ajax('/domain/obj/store', {
success: function(obj) {
$.ajax('/domain/child/store', {
success: function(child) {
assertThat(child.getStuff()).isNotEmpty();
}, ...
});
},
type: 'PUT',
dataType: 'json',
data: JSON.stringify({ key: 'value', key2: 'value2' })
});
Dus een callback wrappen in een callback wrappen in een callback.
Async event loop hulpjes
Zie ook Philip Roberts: Help, I'm stuck in an event-loop
Tooltje om event loop te visualiseren zodat je ziet wat er gebeurt. Breaken in chrome helpt natuurlijk ook, gewoon naar de call stack kijken...
Asynchroon testen in Jasmine
Met Jasmine is het (beperkt) mogelijk om te wachten tot dat een stukje werk uitgevoerd is voordat de assertions afgegaan worden.
Dit kan op de volgende manier:
it("should be green, right??", function() {
var asyncCallFinished = false;
function callback(someObj) {
asyncCallFinished = true;
}
doAsyncCall(callback);
waitsFor(function() {
return asyncCallFinished ##### true;
});
runs(function() {
expect(stuff).toBeLotsBetter();
});
});
Pitfalls:
- Ge moet closure scope gebruiken om een variabele bij te houden om te controleren of de async call klaar is in een callback
- Ge moet
waitsFor()
gebruiken, intern pollt Jasmine waarschijnlijk gewoon... - Ge moet eigenlijke assertions wrappen in
runs()
omdatwaitsFor()
direct retourneert en opzich async is.
De assertion functiepointers die meegegeven worden met runs()
worden intern opgeslaan en bijgehouden totdat de closure van waitsFor()
true
retourneert. Daarna wordt ook alles pas meegegeven met de Jasmine reporter (logging, output etc). Redelijk omslachtig, aangezien 3+ async calls dan waitsFor()
moeten wrappen. Geen oplossing.
Asynchroon testen met QUnit
asyncTest("should be green, right??", function() {
var stuff = gogo();
function callback(obj) {
equal(obj.stuff, 2);
start();
}
doAsyncCall(callback);
});
Pitfalls:
- In de callback van uw async stuk code moeten zoals verwacht uw assertions zitten
- Ge moet een speciale test method gebruiken,
asyncTest()
- Ge moet na uw assertions
start()
aanroepen (??)
De oplossing
https://github.com/willconant/flow-js e.a. (of iets zelfgemaakt in die aard).
Herneem bovenstaande integratietest code in javascript, maar dan met flow.js geschreven:
flow.exec(
function() {
$.ajax('/domain/obj/store', {
success: this,
type: 'PUT',
dataType: 'json',
data: JSON.stringify({ key: 'value', key2: 'value2' })
});
},
function(obj) {
$.ajax('/domain/child/store', {
success: this, ...
});
},
function(child) {
assertThat(child.getStuff()).isNotEmpty();
}
);
Pitfalls:
- Error handling wordt opgefreten - gebruik Firebug's debug on all errors knop in de console. (anders mechanisme maken dat ze doorgooit van closure1 naar 2 ea)
curry()
gaat niet lukken aangezien dethis
pas in de closure zelf de juiste waarde krijgt.this
moet meegegeven worden als callback, dus te intensief gebruik makend vanthis
pointer in eigen code kan BOEM geven.
flow.js geeft het resultaat van closure1 mee als argument aan closure2 (via arguments
) en zo maar door, dat is mega handig.
Synchrone code code combineren met asynchrone in flow.js
Enige minpunt is dat de callback this()
moet expliciet aangeroepen worden om van closureX naar closureY over te stappen!
Los dit op met een utility functie:
flow.sync = function(work) {
return function() {
this(work.apply(this, Array.prototype.slice.call(arguments, 0)));
}
}
Zodat we dit kunnen doen:
flow.exec(
function() {
asyncStuff(this);
},
flow.sync(function(resultfromPrev) {
console.log("lol"); // no this() required afterwards
}),
function(resultFromSyncStuff) {
doMoreAsyncStuff(this);
}
);
In een asynchrone closure parallel werken
Gebruik this.MULTI()
als callback ipv this
(zie voorbeeld hieronder)
flow.js combineren met Jasmine
Om de smeerlapperij van waitsFor()
weg te werken kunnen we ook flow.exec
gebruiken.
❗ De laatste stap gaat altijd een runs()
moeten bevatten voor de reporter om aan te duiden dat assertions uitgevoerd worden, aangezien de exec()
functie direct retourneert. Dus moeten we 1x wachten, totdat de hele "flow" gedaan is. We kunnen dit combineren met BDD en een mini-DSL hierrond schrijven. Resultaat:
function when() {
var flowDone = false;
var slice = Array.prototype.slice;
var argsArray = slice.call(arguments, 0);
var laatsteArgumenten;
argsArray.push(function() {
laatsteArgumenten = slice.call(arguments, 0);
flowDone = true;
});
flow.exec.apply(this, argsArray);
waitsFor(function() {
return flowDone ##### true;
});
return {
then: function(assertionsFn) {
runs(function() {
assertionsFn.apply(this, laatsteArgumenten);
});
}
};
}
Voorbeeldcode:
describe("plaatsen domein", function() {
it("wanneer ik alle plaatsen ophaal, kan ik hier domeinspecifieke functies aan opvragen", function() {
var plaatsen;
when(
function() {
DOMEIN.plaatsRepository.bewaarPlaats(plaats, this.MULTI());
DOMEIN.plaatsRepository.bewaarPlaats(anderePlaats, this.MULTI());
},
function() {
DOMEIN.plaatsRepository.haalPlaatsenOp(this);
}
).then(
function(opgehaaldePlaatsen) {
opgehaaldePlaatsen.forEach(function(plaats) {
expect(plaats.geefMeting).toBeDefined();
});
}
);
});
});
Merk op dat de closure meegeven in then()
(slechts 1 mogelijk voor assertions) als argument het resultaat van de laatste closure in when()
meekrijgt!
jQuery 1.6: Deferred en piping
Vanaf jQuery 1.6 is het mogelijk om met $.Deferred
te werken, dat het mogelijk maakt om een closure uit te voeren op het moment dat "werk" gedaan is. Met werk bedoelen we:
- fx:
.animate
ea - http:
.ajax
ea - custom code die zelf een
$.Deferred
object retourneren
Promising stuff
Alle async operaties worden aan een queue toegevoegd van het jQuery element zelf. Je kan op eender welk moment vragen aan dat queue object, dat wanneer alle items zijn verwerkt er iets speciaals uigevoerd wordt:
$('#blink').fadeOut().promise().done(function() {
console.log('done blinking!');
});
Dit kan dus ook met $.ajax
.
Zelf Deferred code schrijven
Maak een deferred object aan door $.Deferred()
aan te roepen. Op dat moment kan je done()
hierop zoals in het vorige voorbeeld aanroepen. Bijvoorbeeld:
function startStuff() {
var df = $.Deferred();
setTimeout(1000, function() {
console.log('done async call');
df.resolve();
});
return df.promise();
}
startStuff().done(function() {
console.log('really really done with "start stuff"!');
});
Multiple elements in queue: piping
Stel dat eerst element #1 animatie triggert, dan #2, en daarna nog logica dient te gebeuren. Dit kan ook met $.Deferred
, door .pipe()
te gebruiken om verder te breiden aan de queue.
$("button").bind( "click", function() {
$("p").append( "Started...");
var div1 ###### $("#div1"), div2 $("#div2");
var df = $.Deferred();
df.pipe(function() {
return div1.fadeOut("slow")
}).pipe(function() {
return div2.fadeOut()
}).done(function() {
$("p").append( " -- DONE");
});
df.resolve();
});
❗ Pas op, in de .pipe()
functie moet een nieuw promised object geretourneerd worden! Dat nieuw object wordt als return value van de vorige pipe gebruikt. Op die manier wordt er dus chaining toegepast.
Javascript Inheritance
JavaScript uses prototypal inheritance. This means that Javascript does not distinguish between classes/prototypes and instances and, therefore, we can add our desired behavior directly to the instance.
"new" operator
Zie http://unitstep.net/blog/2008/01/24/javascript-and-inheritance/
Gegeven de volgende functie:
function X(bla) {
this.x = bla;
console.log("doeiets");
this.ding = function() { return this.x; };
}
Wat gebeurt er bij de klassieke manier van een "klasse" initialiseren? Zo:
new X("argument").ding()
Omdat geen klassen bestaan, wordt er eigenlijk een "leeg" object aangemaakt en het prototype van het bestaand object aangepast door call
te gebruiken:
var johnDoe = function(){};
X.call(johnDoe, "dinges")
johnDoe.ding()
johnDoe.x ###### "dinges"
Wat gebeurt hier?
function(){}
is een closure, dus een function, zonder inhoud.- de
call
functie roept een functie aan en vervangt dethis
referentie (context) door het meegegeven argument - Bijgevolg wordt in
X()
,this.x = bla
gedaan, dat wil zeggen dat vanaf nu onze anonieme closure de propertyx
bevat, samen met alle functies die binnenX
gedefinieerd zijn.
Merk op dat "bevat" impliceert dat het object johnDoe natuurlijk nu ook geheugen toegekend krijgt om de variabele "x" op te slaan. Dit in contrast met prototypal inheritance, zie volgend stuk.
Eender welke functie heeft een prototype
. Een "lege" functie bevat een dynamische "constructor" (de functie zelf) met lege body:
(function(){}).prototype
Gewenst gedrag - wat waar plaatsen
- Indien ik een functie of een variabele heb die anders kan zijn naargelang de implementatie (definiëer de "naamgeving"), plaats deze dan in de constructor functie.
- Indien ik een functie of een variabele heb die specifiek voor die functie is en niet gaat veranderen, plaats deze dan in het concreet object via
this.
. - Indien ik een functie of een variabele heb die ik tijdelijk wens te gebruiken, plaats deze dan in het concreet object via
var
((Maak slim gebruik van scope chaining om zaken te encapsuleren!)).
Typisch bevatten constructor functies ook geen return waarden ((dit retourneert dus impliciet undefined
)) - we gebruiken deze functies toch altijd in combinatie met de new
operator, dus kennen de nieuwe instantie van het object direct toe aan een variabele.
prototype gebruiken als inheritance
-> Meer informatie inclusief grafen met uitgebreide uitleg hoe prototype en constructors werken: zie http://joost.zeekat.nl/constructors-considered-mildly-confusing.html
Aangezien we aan objecten hun functions kunnen via .prototype
, is het niet moeilijk om een object zich te laten gedragen als zijn "ouder". Neem bijvoorbeeld een dier object, en een concrete ezel implementatie die een extra functie "balk" definiëert.
function Vierpotige() {
this.aantalPoten = 4;
this.eetIets = function() {
console.log("omnomnom");
}
}
function Ezel() {
this.balk = function() {
console.log("IEE-AA enzo");
}
}
Ezel.prototype = new Vierpotige();
var ezeltje = new Ezel();
ezeltje.eetIets(); // aha! outputs 'omnom' en 'als ezel fret ik ook nog gras enzo'
Wat gebeurt hier precies?
- Vierpotige bevat een property aantalPoten en een functie eetIets.
- de constructor functie Vierpotige wordt aangeroepen zodra een Ezel aangemaakt wordt, zodat properties en functies overgenomen worden.
- een nieuwe ezel eet iets via een prototype functie van een lege Vierpotige.
❗ Opgelet Vierpotige.prototype bevat enkel de constructor functie zelf en NIET "eetIets", daarom dat we Ezel.prototype gelijk stellen aan een nieuwe lege vierpotige. Dit zou niet werken:
Ezel.prototype = Vierpotige.prototype;
new Ezel().eetIets() // kapot
Tenzij we eetIets definiëren via Vierpotige.prototype.eetIets = function() { ... }
- maar dan kan aantalPoten
niet meer vanaf een ezel accessed worden.
Nu de prototype property van Ezel en Vierpotige gelijk zijn, kunnen we het prototype uitbreiden met een functie en die direct op ons nieuw ezeltje toepassen:
Vierpotige.prototype.verteer = function() {
console.log("ist wc bezet??")
}
ezeltje.verteer()
❗ Waarschuwing het prototype van Vierpotige
aanpassen past ook elke Ezel
instantie aan, dit is énorm gevaarlijk als er publieke properties zoals aantalPoten
gedefiniëerd zijn! Want in tegenstelling tot wat velen denken, worden properties niet gekopiëerd! Dus dit zou het aantal poten van ALLE ezels aanpassen:
dier = new Vierpotige();
Ezel.prototype = dier;
new Ezel().aantalPoten ###### 4; // true
dier.aantalPoten = 2;
new Ezel().aantalPoten ###### 4; // false
Properties overriden
Prototypal inheritance werkt omdat JS bij elke property lookup kijkt in welk object die referentie gedefiniëerd is. Is dat het huidig object, neem dan die waarde. Indien neen, kijk in het prototype
object. Indien neen, kijk in het prototype
object van dat object, en zo maar door tot op Object
niveau. We kunnen zo ook een property van een prototype zelf overriden, door ander gedrag te definiëren, of zelfs de super aan te roepen:
Vierpotige.prototype.eetIets = function() {
console.log("vierpotige eten");
}
Ezel.prototype.eetIets = function() {
Vierpotige.prototype.eetIets(); // "super"
console.log("als ezel fret ik ook nog gras enzo");
}
Built-in JS types extenden
❗ Extend nooit Object.prototype
! Waarom? Omdat Eender welk object een instantie van Object
is, dus zijn prototype heeft, en met een for(x in prop)
deze property nu ineens toegankelijk is voor elk object. Een leeg object { }
wordt verwacht géén properties te hebben!
Object.prototype.hack = function() {}
for(x in {}) {
console.log(x); // print "hack", zou hier niet in mogen komen
}
Checken op inheritance
Met Javascript kan men door middel van typeof
controleren van welk type een variabele is. Dat komt neer op:
- object (
{}
) - function (
function() {}
) - string (
""
) - number (
-47.2
)
Het is niet zo interessant om te gebruiken voor eigen inheritance. Daarvoor dient instanceof
:
ezeltje instanceof Vierpotige == true
ezeltje instanceof Ezel == true
new Vierpotige() instanceof Ezel == false
Zelf inheritance afchecken met prototype
met constructors
Dit is een zeer beperkte manier dat geen rekening houdt met "inheritance":
function instanceOf(One, Two) {
return One.constructor ###### Two;
}
instanceOf(Ezel, Vierpotige) // true
Aangezien elk object een .constructor
property heeft die afgeleid werd vanuit de constructor functie die aangeroepen werd, kan men op deze manier een simpele check afwegen. Een praktischer voorbeeld is (typeof (new Date()) ###### object) && (new Date().constructor ###### Date)
.
met proto
Het instanceof
keyword kijkt natuurlijk naar de prototype
properties van beide objecten om te controleren of object a van object b komt. Dit kan men ook zelf doen:
function instanceOf(One, Two) {
return One.prototype.__proto__ ###### Two.prototype;
}
instanceOf(Ezel, Vierpotige) // true
Hoe kan dit kloppen?
- Herinner u dit statement:
Ezel.prototype = new Vierpotige();
. Dit stelt deprototype
van Ezel gelijk aan die van een vierpotige. Het enige wat in deprototype
van Vierpotige steekt is deverteer()
functie, de rest werd via de constructor functie overgekopiëerd! - De magic property
_ _proto_ _
wordt intern gezet zodra een prototype wordt toegekend aan een object. Aangezien Ezel zelf ook prototype functies heeft specifiek voor ezels, kunnen we die van Vierpotige niet overriden, maar wel gebruiken.
❗ Bekijk iets toevoegen via .property
als iets toevoegen aan het algemeen prototype object, en iets rechtstreeks toevoegen via een key als iets toevoegen op een instance van dat prototype object. In andere dynamische talen stelt property
de metaClass
voor, maar JS werkt enkel met functies.
De betere oplossing: isPrototypeOf()
! Zie magic properties.
❗ _ _proto_ _
is een instance property, .prototype
een constructor function property
met properties
Door hasOwnProperty()
te gebruiken kan je nagaan of een property overgenomen is of niet. Vanaf JS 1.5.
call als inheritance
De klassieke inheritance structuur zoals in Java en C++ kan beter benaderd worden door call
te gebruiken. Herbekijk onze ezels:
function Vierpotige() {
this.aantalPoten = 4;
this.eetIets = function() {
console.log("omnomnom");
}
}
function Ezel() {
Vierpotige.call(this);
this.balk = function() {
console.log("IEE-AA enzo");
}
}
var ezeltje = new Ezel();
ezeltje.eetIets(); // aha! outputs omnom
Door als eerste statement in de constructor functie van Ezel
een call
te plaatsen naar onze "parent", kopiëren we alle keys en values die daarin gedefiniëerd staan. In tegenstelling tot prototypal inheritance kost dit dus veel meer geheugengebruik, en is dit beperkter om uit te breiden. We linken eigenlijk impliciet twee functies aan elkaar door waarden over te nemen, maar iets aanpassen aan Vierpotige
gaat de Ezel
op geen ekele manier doen veranderen.
prototypal inheritance toepassen
In plaats van new
overal te gebruiken zonder te weten wat hierachter ligt, kan men create
ook gebruiken:
if (typeof Object.create !###### 'function') {
Object.create = function (o) {
function F() {}
F.prototype = o;
return new F();
};
}
newObject = Object.create(oldObject);
Zie http://javascript.crockford.com/prototypal.html
Een minder verbose manier om extra properties te definiëren
Zie http://howtonode.org/prototypical-inheritance -
Object.defineProperty(Object.prototype, "spawn", {value: function (props) {
var defs = {}, key;
for (key in props) {
if (props.hasOwnProperty(key)) {
defs[key] = {value: props[key], enumerable: true};
}
}
return Object.create(this, defs);
}});
Op die manier kan je BaseObj.spawn({'extraProp': 'extraValue'});
gebruiken, zonder de relatieve verbose manier van extra properties te moeten gebuiken die Object.create
handhaaft.
Prototype JS en Class.create
Javascript frameworks proberen altijd inheritance makkelijker te maken voor klassieke OO developers door functies te modelleren als klassen. In Prototype JS kan men zoiets doen:
var Animal = Class.create({
initialize: function() {
this.something = "wow";
},
speak: function() {
console.log(this.something);
}
});
var Snake = Class.create(Animal, {
hiss: function() {
this.speak();
}
});
Snake leidt af van Animal, en de initialize()
functies stellen de constructor functies voor. Wat gebeurt er dan achter de schermen bij Class.create({...})
?
- Net zoals hierboven wordt de constructor functie via
apply
aangeroepen (zelfde als call). Enkel wordt alsthis
een lege functie toegevoegd. Object.extend()
wordt gebruikt om alle keys van de parent te kopiëren naar de nieuwe lege functie.- De
prototype
property van de parent wordt net zoals hierboven gezet op de nieuwe "klasse". - Speciale gevallen zoals een "lege constructor" functie indien nodig, intern bijhouden wat sub- en superklassen van elkaar zijn, etc.
In essentie komt het neer op "syntax sugaring" zodat het klassieke OO model gebruikt kan worden - terwijl er onderliggend iets anders gebeurt.
Meer info over deze implementatie: http://code.google.com/p/inheritance/
############= Multiple inheritance ############=
Perfect mogelijk, of slechts delen van object A en alles van B voor object C gebruiken (mixins!). Simpelweg alles van de ene prototype
property naar de andere overzetten wat nodig is:
######= Methode 1 ######=
function A(){};
A.prototype.a = function() { return "a"; }
A.prototype.c = function() { return "a"; }
function B(){};
B.prototype.b = function() { return "b"; }
function C(){};
for(prop in B.prototype) {
C.prototype[prop] = B.prototype[prop];
}
C.prototype.a = A.prototype.a;
C.prototype.c = function() { return "c"; }
var c = new C();
c.a() ###### "a";
c.b() ###### "b";
c.c() ###### "c";
######= Methode 2 ######=
Creeër de illusie om constructor(s) aan te roepen in een constructor functie van een ander object:
function B() {
this.b = function() { return "b"; }
}
function C() {
this.c = function() { return "c"; }
}
function A() {
this.a = function() { return "a"; }
this.super = B;
this.super2 = C;
this.super(); // kopiëer de b functie in A, maar inherit niet!
this.super2();
}
var a = new A();
a.a() ###### "a";
a.b() ###### "b";
a.c() ###### "c";
######= Problemen met mixins ######=
❗ Dit is géén authentieke multiple inheritance. Hier zijn twee problemen aan gekoppeld:
- Zodra via
B.prototype
een nieuwe functie toegevoegd wordt, zal C deze niet overpakken omdatC.prototype
niet gelijk gesteld werd aan die van A of B - En bijgevolg dus ook de
instanceof
operator naar de zak is:
c instanceof C ###### true
(c instanceof B || c instanceof A) ###### false
Als dit echt nodig is kan men zoiets zelf implementeren door weer te loopen via for(prop in x.prototype) { ... }
en te checken of alle keys voorkomen in een object.
Zie Mozilla Dev center: Details of the Object Model
Javascript Scoping
Toplevel Scope
Een stuk Javascript in een HTML pagina, zonder eender welke functie te definiëren, werkt altijd op top-level scope. Dat stelt het window
object voor, waar we ook resolutie gegevens en dergelijke kunnen uithalen. Dus simpele variabelen declareren om in HTML te kunnen gebruiken werkt altijd op de globale scope:
var customerEmail = document.getElementById("username") + "@" + document.getElementById("domain") + ".com";
window.customerEmail // what did I do?? (null@null.com if unknown IDs)
Om te vermijden dat we alles op het window
object "dumpen", schrijven we nette functies die zaken zoals tijdelijke variabelen en private stukken code encapsuleren.
Variabele declaratie
Variabelen definiëren gaat met var
(zoals hierboven), máár globale (window-scope) variabelen kunnen gedeclareerd worden zonder dit. Pas hiermee op:
woot = "leet";
function yo() {
woot = "omg"; // whoops, I changed a global var
meerWoot = "leet";
var wootwoot = "one!!1";
}
yo();
window.woot ##### "leet" // false
window.meerWoot ##### "leet" // true
window.wootwoot ##### undefined // true
Dit verklaart de nood om var
te gebruiken om lokale variabelen te definiëren.
Merk op dat hier wootwoot
énkel binnen de functie yo()
leeft, dus via de Javascript Garbage Collector weggesmeten wordt zodra die functie volledig geëvalueerd is.
#####= Nested en Block scope #####=
Functies in functies in functies zijn perfect mogelijk, en de binnenste functies hebben toegang tot de scope van alle anderen.
❗ In tegenstelling tot Java e.a. beschikt JS niet over block scope. Dit wil zeggen:
var f1 = 10;
function f() {
console.log(f1); // 10? Nope, undefined!
var f1 = 1;
function z() {
var z1 = 2;
if(f1 ###### 1) {
var z2 = 2;
}
return z1 + z2; // z2 nog steeds toegankelijk
}
}
Waarom logt dit undefined
, terwijl op global scope aan f1 10 toegekend wordt? Omdat een tweede variabele genaamd f1 in de body van de functie gedeclareerd wordt, wordt die versie gebruikt, ook al is deze nog niet toegekend!. Wow.
JS Is dus buiten de lexicale scoping ook nog eens function-level scoped.
Een duidelijker voorbeeld via http://www.adequatelygood.com/2010/2/JavaScript-Scoping-and-Hoisting:
#include <stdio.h>
int main() {
int x = 1;
printf("%d, ", x); // 1
if (1) {
int x = 2;
printf("%d, ", x); // 2
}
printf("%d<br/>n", x); // 1
}
Print logischerwijze 1, 2, 1, juist? Doe hetzelfde eens in javascript:
var x = 1;
console.log(x); // 1
if (true) {
var x = 2;
console.log(x); // 2
}
console.log(x); // 2
BOOM.
Dit komt doordat if
statements geen nieuwe scope introduceren, enkel function
definities! Een oplossing is een anonieme functie gebruiken en die direct evalueren:
function foo() {
var x = 1;
if (x) {
(function () {
var x = 2;
// some other code
}());
}
// x is still 1.
}
Scope chain
Elke JS Executie context heeft een scope chain toegekend. Dit is een lijst van objecten waar de interpreter door gaat om een variabele x op te kunnen zoeken (Dit proces heet variable name resolution). Men begint met de huidige context van de functie die opgeroepen wordt. Indien variabele x daar niet gedefiniëerd is, ga een scope hoger, en zo voort.
In top-level JS (op window scope) bevat de scope chain slechts één object, het "globaal" object. (window
)
Event handler scope chain
Bij het uitvoeren van events in de DOM Tree zitten er buiten window
nog enkele andere objecten op de scope chain: het object dat het event zelf triggerde. Het is dus mogelijk om rechtstreeks vanuit een onclick
event van een input
tag, een ander form
element aan te spreken zonder dit eerst te resolven via de klassieke getElementById()
:
<head>
<script>
function load() {
document.getElementById("text").onclick = function() {
alert(anders.value);
}
}
</script>
</head>
<body onload="load();">
<input type######"text" id"text" value="blabla" />
<input type######"text" id"anders" value="blieblie" />
</body>
merk op dat anders.value
mogelijk is doordat het DOM element text mee op de scope chain zit.
Dit is vanzelfsprekend serieus verwarrend en bad practice, Firebug waarschuwt hier ook voor:
Element referenced by ID/NAME in the global scope. Use W3C standard document.getElementById() instead.
Dit kan serieuze problemen met zich mee brengen, zeker wanneer mensen niet goed weten hoe javascript te gebruiken en bijvoorbeeld for loopjes schrijven door variabelen op toplevel scope te introduceren:
<head>
<script>
function Iets(e) {
return function() {
alert(j); // DOM Element: Div met id "j", al hebben we nergens j gedefiniëerd!!!
// j = 0: BOOM in IE: Object doesn't support this property or method
// reden => in IE is dit een readonly property, Gecko parsers zijn lakser hierin
for(j = 0; j < 10; j++) {
// whatever
}
alert(j); // 10
}
}
</script>
</head>
<body>
<h2>Javascript event scopechain voorbeeld</h2>
<div id######"j" onclick"Iets()()">
Klik hierop aub
</div>
</body>
Scope tijdelijk aanpassen
with(window.screen) {
console.log(width);
}
Al is var screen = window.screen; screen.width
natuurlijk even makkelijk.
#####= Private Member variables #####=
❗ "Functies en variabelen in objecten zijn overal en altijd toegankelijk". DIT IS FOUT! Bekijk het volgende voorbeeld (zie private members docs):
function Func(param) {
this.publicMember = param;
var privateMember = 10;
var me = this;
this.publicFunction = function() {
console.log(this.publicMember);
};
// could be function privateFunction() {
var privateFunction = function() {
console.log(privateMember);
console.log(this.publicMember); // BOOM
console.log(me.publicMember); // OK
};
}
new Func(10).privateFunction() // BOEM
Zodra we this
gebruiken om members in een constructor functie te steken wordt het publiek. Zodra we gewoon variabelen definiëren, ook al zijn het closures zoals privateFunction
, is dit niet toegankelijk voor de buitenwereld! Dit wil zeggen dat het zelfs niet toegankelijk is voor .prototype
-toegevoegde functies.
Merk op dat we aan privateMember
kunnen zonder this
te gebruiken om naar iets te refereren. Dit komt omdat die members in de context van het object zitten.
Een probleem dat zich voordoet is dat de this
pointer binnen private functions natuurlijk weer gereset wordt tot op window
scope. Om dit op te lossen kunnen we een private variabele voorzien die refereert naar this
, daarvoor dient me
.
Private, Public en prototype functies
Een private functie is een functie die in de constructor functie gedefiniëerd is als member variabele, en dus geldig is binnen de context van die functie.
Een privileged functie is een functie die in de constructor functie gedefiniëerd is met de this accessor. Deze functies kunnen private functies aanroepen omdat ze binnen de context van de constructor functie leven, en zijn ook aanroepbaar van buitenaf.
Een public functie is een functie die in het prototype leeft van een object((privileged en public zijn in feite gelijk, onderscheid wordt gemaakt om public variables te scheidden)). Private members zijn hier niet toegankelijk, privileged wel. Een voorbeeld (vervolg):
Func.prototype.publicThing = function() {
alert(this.publicMember);
return this.privateMember; // BOOM
};
#####= Object/Class Member variables #####=
In typische OO talen zoals Java en C++ kunnen ook "statics" gedefiniëerd worden die enkel op klasse niveau leven. Zoiets is heel simpel te realiseren met Javascript, door een property op de constructor functie zelf te steken:
function Const() {
this.c = "const";
c = "globalConst";
}
Const.C = "C";
c ###### "globalConst"; // remember, window scope
new Const().c ###### "const";
new Const().C != undefined
Dit noemen we "class" properties, in plaats van "instance" properties.
#####= Anonieme functies gebruiken om members private te maken #####=
Probleem: publieke functies die aan een object hangen met this.functie = function() { ... }
zijn toegankelijk. Ik wil iets groeperen zonder window
scope te vervuilen. Hoe kan ik één functie opsplitsen zonder de andere te exposen?
Oplossing:
var Stuff = (function() {
function doeStap1() {
console.log("private things in here");
return 3;
}
function doeStap2Met1(een) {
return een * 2;
}
return {
doeStuff: function() {
return doeStap2Met1(doeStap1());
}
}
})()
console.log(Stuff); // outputs Object met "doeStuff" key
console.log(Stuff.doeStuff()); // 6
In JavaScript, as opposed to statically scoped languages, all variables are scoped to the function in which they're defined (not the "block" as defined by curly braces). The above code snippet creates an anonymous function and immediately executes it. This has the effect of creating a scope in which variables can be defined, and anything in the containing scope is still accessible.
Zie http://trephine.org/t/index.php?title=Aspect_Oriented_JavaScript
Wat gebeurt er?
- Wrap een lege functie in Stuff, waar x aantal functies in zitten die wat werk doen.
- In plaats van dat toe te kennen aan Stuff, evalueer direct de nieuwe functie met
(function() { ... })()
. Wat terugkomt is een closure functie diedoeStuff
definieert, de rest is niet zichtbaar.
#####= Expliciet objecten binden met call/apply #####=
Neem als voorbeeld terug de ezel. Die zal bij het balken "iaia met mijn 4 poten" op de console afdrukken. Zodra de balk functie aangeroepen wordt, bind Javascript de ezel aan het this
keyword, zodat poten correct opgezocht kan worden. Stel nu dat ik een spin wil laten balken, zonder de spin de balk functie te laten refereren/mixen vanuit de ezel:
var Spin = function() {
this.poten = 8;
};
var tarantula = new Spin();
new Ezel().balk.call(tarantula); // iaia met mijn 8 poten
Wat gebeurt hier?
- Ik maak een nieuw object aan met ook een poten property
- balk gebruikt de poten property maar ik wil mijn spin gebruiken in plaats van de 4 poten van de ezel!
- gebruik
call
om balk uit te voeren, en geef als argument mijn spin mee, zodat de balk functie gebind wordt op mijn spin in plaats van de nieuwe ezel instantie
Het is ook mogelijk om zonder argument call
uit te voeren:
var poten = 100;
new Ezel().balk.call(); // iaia met mijn 100 poten
Hoezo 100? this
wordt dan window
, de hoogst mogelijke scope, en daar is net toevallig ook een poten variabele op gedefiniëerd. Als dat niet zo was gaf dit als output "iaia met mijn undefined poten".
Impliciete unbound objecten
Het vorige voorbeeld toont aan hoe je expliciet this
kan "unbinden". Dit gebeurt ook regelmatig intern impliciet, bijvoorbeeld met setTimeout
of met events zoals blabla.onclick
.
De oplossing hiervoor is closures gebruiken. Bekijk dit voorbeeld:
function SomeClass() {
this.message = "hallo";
this.startLooping = function() {
setInterval(this.doeBijInterval, 1000);
};
this.doeBijInterval = function() {
console.log(this.message); // BOOOEEMM
}
}
new SomeClass().startLooping();
Wat loopt hier mis? ((Buiten het feit dat setInterval niet altijd mag uitgevoerd worden, er moet een guard clause rond, setInterval retourneert een id!))
doeBijInterval wordt hier om de seconde uitgevoerd, en this.message wordt afgedrukt. this
verwijst op dat moment niet meer naar de instantie van SomeClass!
De oplossing, een closure meegeven aan setInterval
die kan werken op de instance:
function SomeClass() {
this.message = "hallo";
this.startLooping = function() {
var instance = this; // OK, SomeClass instance
setInterval(function() {
instance.doeBijInterval(); // this = window scope, gebruik de instance var.
}, 1000);
};
this.doeBijInterval = function() {
console.log(this.message); // Da werkt ofwa
}
}
new SomeClass().startLooping();
loop closures
Een ander voorbeeld waar het mis kan gaan (ref. http://trephine.org/t/index.php?title=JavaScript_loop_closures !):
var list = [ 'a', 'b', 'c' ];
for (var i######0, llist.length; i<l; i++) {
var item = list[i];
setTimeout( function(){ alert(item); }, 1000 ); // print 3x 'c'
}
Hoe kan dit 3x 'c' afdrukken en niet 'a', 'b', 'c'?
- In de lus ken ik aan item de huidige index van de array toe.
- Voer een functie uit die een seconde in de toekomst uitegevoerd wordt. [NOG NIET]. Repeat lus tot klaar.
Zodra de events verwerkt worden, is item reeds de laatste in de rij. Oei. De oplossing, weeral closures:
var list = [ 'a', 'b', 'c' ];
for (var i######0, llist.length; i<l; i++) (function(item){
setTimeout( function(){ alert(item); }, 1000 );
})(list[i]);
Hier wordt een anonieme functie aangemaakt die direct geëvalueerd wordt met list[i]
als parameter, zodat elke closure uniek gebonden is aan de juiste parameter.
Zie AOP hieronder voor meer uitleg over anonieme functies.
#####= Samenvatting: module pattern #####=
Wanneer we alle bovenstaande technieken toepassen, krijgen we typisch in Javascript iets zoals dit:
var MyGlobalNewModule = (function(module) {
var privateVar = 3;
function privateFn() {
privateVar += 3;
}
// decorate or use module here if wanted.
return {
publicProperty: "hello",
publicFn: function() {
return privateFn() - 2;
}
};
})(GlobalModule);
Wat zit hier in verwoven?
- private variables en functions zitten in de anonieme functie scope
- andere modules zijn toegankelijk via een argument, en niet via de directe variabele
- publieke functies worden exposed via een object dat teruggegeven wordt.
Zo werken bijvoorbeeld de API en plugins van jQuery.
❗ Anonieme functies declareren en uitvoeren kan op twee manieren in JS: via (function() { ... })()
en via (function() { ... }())
. Merk het verschil in haakjes op. Het resultaat is exact hetzelfde, er is alleen een semantisch verschil, namelijk dat bij de eerste expressie de haakjes de functie expressie vasthoudt, en bij de tweede expressie de call expressie (het resultaat van de functie)
Zie stackoverflow voor meer uitleg - schematisch:
CallExpression
| |
FunctionExpression |
| |
V V
(function() { }());
^ ^
|--PrimaryExpression --|
VS
PrimaryExpression
|
FunctionExpression
|
V
(function() { })();
^ ^
|-- CallExpression --|
Testing JS Code
Testen schrijven
Klassieke Unit testen
Frameworks gebruiken zoals jQuery's QUnit die het makkelijk maken modulair te testen:
test("dit zou dat moeten doen blabla", function() {
equal(expected, actual);
ok(someThingExpectedToBeTrue);
});
Mocking van bijvoorbeeld $
mogelijk via MockMe, een door Mockito geïnspireerde Javascript Object spy API. Iets van:
when(f)('in').thenReturn('out');
assertEqual('out', f('in'));
❗ Vereist Prototype JS
, en JsUnit
om de assert
functions te kunnen gebruiken
Gedrag testen met Jasmine
Inspiratie van de Ruby community gehaald, met name RSpec-based.
Frameworks gebruiken zoals Jasmine ((Niet afhankelijk van andere JS Frameworks, vereist geen DOM dus geen Envjs nodig serverside!)):
describe("Rekenmasjien", function() {
it("should add one number", function() {
var calc = new Calc(0);
expect(calc.increase()).toEqual(1);
});
});
Krachten:
- Schrijf code door expectations (Matchers vbs) op te bouwen, in dezelfde trand als
FESTAssert
. - Bundel expectations in een suite ("beschrijvend")
- Schrijf makkelijk extenties om eigen expectation functions op te bouwen als de standaard API niet volstaat
beforeEach()
enafterEach()
functions binnen suites- Snel disablen van testen via
xit()
(@Ignore
zogezegd) - mocking en spying heel eenvoudig mogelijk: zie https://github.com/pivotal/jasmine/wiki/Spies
- Plugins voor reporting, custom matching, ...
Zwakheden:
- Async testen vereist
waitsFor()
ea. Zie onder - Vereist specRunner.html files, standaard altijd in een browser runnen. Genereer specRunners!
Betere matchers speciaal voor jQuery ea zijn beschikbaar: https://github.com/velesin/jasmine-jquery
Aynschrone code testen
Zie async stuff
Jasmine integreren met jsTestDriver
Zie http://skaug.no/ingvald/2010/10/javascript-unit-testing.html
Er is een Jasmine adapter beschikbaar: https://github.com/ibolmo/jasmine-jstd-adapter
Het kan ook handig zijn om een Junit XML Reporter te gebruiken om bijvoorbeeld voor Hudson het makkelijker te maken om de test output files te verwerken. Er zijn reeds enkele reporter plugins zoals deze beschikbaar, hier: https://github.com/larrymyers/jasmine-reporters
❗ Integratie met Hudson, EnvJS en Rhino ea: zie eigen junit test runner: https://github.com/wgroeneveld/jasmine-junit-runner
Andere JS Test frameworks
Interessante links:
Testen automatiseren
Distributed testing in-browser
jsTestDriver
http://code.google.com/p/js-test-driver/
Wat doet dit?
- Een jar dat een server opstart die een browser bestuurt en bepaalde JS files (uw testen en uw productiecode) automatisch inlaadt en uitvoert
- Bevat een API met asserts
- Reportgeneratie met plugins voor eclipse om ze JUnit-like te tonen
- Integratie met build etc mogelijk
- Integratie met Jasmine mogelijk: https://github.com/ibolmo/jasmine-jstd-adapter
jsTestDriver integreren met Hudson
Zie http://cjohansen.no/en/javascript/javascript_continuous_integration_with_hudson_and_jstestdriver
Het komt eigenlijk hierop neer: maak een nieuw target, voer extern dit commando uit:
java -jar test/JsTestDriver-1.2.2.jar <br/>
--config jsTestDriver.conf <br/>
--server http://localhost:4223
--tests all --testOutput . --reset
Dit neemt aan dat de server reeds gestart is, kan met een shell script op de Hudson server bak: java -jar test/JsTestDriver-1.2.2.jar --port 4223
.
JsUnit Server
http://www.jsunit.net/documentation/index.html
Wat doet dit?
- Een jar dat een Jetty Server opstart die een browser bestuurt en bepaalde HTML files inlaadt en uitvoert, waar testen in zitten
- Afhankelijk van
Prototype JS
om asserts uit te voeren - Integratie met ant zeer eenvoudig
TestSwarm
Zie http:*swarm.jquery.org/ en https:*github.com/jquery/testswarm/wiki
Headless testen
Mogelijkheid 1: EnvJS
EnvJS is een gesimuleerde browser omgeving geschreven in JS. Zie http://www.envjs.com/
Voordeel: heel snel
Nadeel: Java
ofzoiets nodig om JS te evalueren (Rhino of V8 in C++ van Google), plus kan onregelmatigheden vertonen met hevig gebruik maken van UI frameworks -> dit zou moeten werken allemaal, maar het blijft een gesimuleerde omgeving.
❗ Zie https://github.com/wgroeneveld/jasmine-junit-runner
Integreren met hudson als extern commando:
java -cp lib/envjs/js.jar:lib/envjs/jline.jar org.mozilla.javascript.tools.shell.Main -opt -1 -f lib/envjs/envjs.bootstrap.js -f test.js
test.js
heeft dan maar 1 regel die naar de juiste specRunner.html gaat met window.location
.
Mogelijkheid 2: Qt Webkit widget
Webkit is een opensource web renderer, en er is een implementatie in Qt
beschikbaar (vereist libs geïnstalleerd te hebben). Zie http://trac.webkit.org/wiki/QtWebKit
Voordeel: gedrag volledig in een "echte" website rendered, in plaats van via een omweg. Aangezien het een Qt Widget is, hoeft het niet expliciet op het scherm gerendered te worden (dit pollt gewoon totdat bvb "Jasmine tests run" ofzoiets van tekst verschijnt, om dan de HTML te retourneren als resultaat.
Nadeel: Qt libs vereist, nog altijd niet volledig "headless", aparte widget spec runner in de achtergrond die draait.
Zie http://johnbintz.github.com/jasmine-headless-webkit/ (Implementatie in Ruby met nogal veel nadruk op gems ea... Handig?)
Phantom JS is een full stack headless browser implementatie gebaseerd op WebKit (C++/Python implementatie).
Pitfalls
❗ Gebruik aub JSLint om onderstaande "probleempjes" makkelijker te kunnen tracen en aanpassen.
Strict mode in ECMA5 is ook iets héél handig.
-> Meer (al dan niet grappige) pitfalls/weetjes: http://www.wtfjs.com/
Objecten
Javascript gebruikt intern de toString()
functie wanneer objecten als keys worden toegekend. Dit wil zeggen dat eigenlijk de string representatie als key gebruikt wordt. Bijvoorbeeld:
var obj = {};
var key1 = new Object();
var key2 = new Object();
obj[key1] = "hi";
obj[key2] = "yo";
console.log(obj[key1]); // ##### yo???
#####= arguments #####=
arguments on-the-fly veranderen
Zonder use strict
(ES5) kan je een binnenkomend argument ook toekennen aan een andere waarde. Hierdoor verandert de waarde van arguments[0]
! De arguments
lijst houdt dus een pointer bij naar de variabele die de value van het argument vasthoudt, en niet naar de value zelf. In strict mode wel. Voorbeeld:
function bla(a) {
a = 5;
console.log(arguments[0]);
}
bla(1); // 5, in strict mode 1
arguments is geen array
Snel "effe" lopen over alle argumentjes:
function aha() {
arguments.forEach(function(itm) { // syntax error ???
console.log(itm);
});
}
aha(1, 2, 3);
Inderdaad: Array.isPrototypeOf(arguments) ##### false
. Dus de .length
property werkt op een andere manier, en van die hidden .callee
dingen (die niet werken in strict mode) zitten er ook op...
Oplossing:
function aha() {
var args = Array.prototype.slice.call(arguments);
args.forEach(function(itm) {
console.log(itm);
});
}
aha(1, 2, 3);
#####= Falsey values #####=
De volgende values evalueren allemaal naar false
in bvb een if()
block:
null
0
alsnumber
""
alsstring
false
alsboolean
undefined
NaN
Deze dingen lukken ook niet:
if(new Boolean(false)) {
alert("ik ben false jong!"); // oei tis nie waar hé?
}
Mogelijke Oplossingen:
- gebruik altijd
.valueOf()
van de object counterparts van alle primitives! - gebruik
Boolean(false)
(zonder new), zie Mozilla Boolean Dev docs
Lees hier meer over in The secret life of Javascript Primitives en the value of valueOf.
#####= Typecasting #####=
^ code ^ resultaat ^ | [] + 23 | de string "23" | | [1, 2] + 23 | de string "1, 223" | | false + 23 | het getal 23 | | true + 23 | het getal 24 | | "kadootje" + 23 | de string "kadootje23" | | undefined + 23 | NaN | | null + 23 | het getal 23 |
#####= Parsing #####=
parseInt("08")
neemt automatisch aan dat de waarde naar octal values omgezet moet worden. Dit evalueert naar het getal 0
.
Oplossing: parseInt("08", 10)
- het tweede argument geeft de eenheid aan (10 = decimaal).
#####= de wereld breken (undefined) #####=
undefined
is een toplevel variabele die als value de primitive value undefined
heeft.
Zodra je undefined
(de variabele) toekent aan iets anders, is die primitive value weg en is alles naar de zak. Voorbeeld:
Dit is opgelost in ECMA5 door sommige properties al dan niet writable te maken. undefined
is daar dus readonly. Check dit in Firefox 4:
var undefinedProps = Object.getOwnPropertyDescriptor(window, "undefined")
undefinedProps.writable ##### false; // locked down!
#####= Javascript en puntkomma's #####=
Geen enkele lijn hoeft een ;
te bevatten, javascript voegt deze automatisch toe bij het evalueren. Dit kan ook miserie veroorzaken:
function geefIets() {
return
{
iets: "wow"
};
}
geefIets(); // undefined: unreachable code!
Los dit op door de accolade na de return te plaatsen in plaats van de newline.
#####= ECMA5: map, forEach, filter: de 3 parameters #####=
Sinds ECMA5 is het héél handig om bijvoorbeeld voor een array van strings elk element individueel te manipuleren en een nieuwe array terug te geven.
Stel, ik wil string
s naar number
s omvormen:
["1", "2", "3"].map(parseInt); // returns [1, NaN, NaN ] ???
Whoops? parseInt
heeft als tweede variabele de eenheid (decimaal, hexa, ...) en alle array manipulatie functies geven altijd 3 argumenten mee: het element, de index en de array zelf. Conflict!
Oplossing: closures!
#####= Arrays zijn "speciale" associatieve objecten #####=
Dit zegt de [http:*bclary.com/2004/11/07/#a-15.4ECMAScript specificatie](http:*bclary.com/2004/11/07/#a-15.4ECMAScript specificatie):
Array objects give special treatment to a certain class of property names. A property name P (in the form of a string value) is an array index if and only if ToString(ToUint32(P)) is equal to P and ToUint32(P) is not equal to 2^32 - 1. Every Array object has a length property whose value is always a nonnegative integer less than 2^32. The value of the length property is numerically greater than the name of every property whose name is an array index; whenever a property of an Array object is created or changed, other properties are adjusted as necessary to maintain this invariant. Specifically, whenever a property is added whose name is an array index, the length property is changed, if necessary, to be one more than the numeric value of that array index; and whenever the length property is changed, every property whose name is an array index whose value is not smaller than the new length is automatically deleted. This constraint applies only to properties of the Array object itself and is unaffected by length or array index properties that may be inherited from its prototype.
Dat wil zeggen dat:
- zodra de
.length
property gewijzigd wordt, er verschillende dingen gebeuren: 1. Indien kleiner wordt: alle properties kleiner dan de lengte worden automatisch verwijderd 2. Indien groter wordt: allemaal nieuwe elementen metundefined
als value (impliciet!) - Alle "integer" values (zie geweldige formule) zijn "speciale" properties die de
length
property ook aanpassen.
Wat gebeurt er als ik de length
property boven het maximum zet?
var arr = [1];
arr.length = Math.pow(2, 32) - 1; // max allowed
arr.length++; // RangeError: invalid array length (na lang denken van de parser)
❗ de length
property wordt gebruikt door utility functies zoals push
, concat
etc, dus als deze niet correct werkt zoals hierboven beschreven gebeuren en vreemde dingen!
Meer info: zie How to subclass an Array