Advanced javascript

Scope en javascript compiler

Scope: 
waar zoeken we naar (variabelen, functies, etc)
wie zoekt er?

Javascript heeft enkel een function scope.

Misconceptie is dat Javascript geen compiled language is; dat is het namelijk wel. De JS engine gaat twee keer door de code heen: de compileer-fase en de runtime fase. Hierover later meer.
Compileerfase zoekt naar declaraties van variabelen en functies en plaats deze in de juiste 'scope slots'.

var foo = "bar";

function bar(){
  var foo = "bar";
}

function baz(foo){
  foo = "bam";
  bam = "yay";
}

Deze code wordt door Javascript door twee totaal verschillende fases verwerkt:
De declaration fase (het var foo gedeelte) en een initialization fase (het toekennen van "bar" aan foo). Deze twee operaties (fases) gebeuren op totaal verschillende tijdstippen.

De compiler scant naar alle declaraties (var statements en function declarations) die hij kan vinden. Als hij een declaratie heeft gevonden, kijkt hij in welke scope hij nu zit. In bovenstaand voorbeeld zit de declaratie in de global scope. Vervolgens registreert hij foo in de global scope. Op dezelfde manier registreert hij de functies bar en baz ook in de global scope. Als een functie wordt geregistreerd, wordt ook in de functie gezocht naar declaraties. Binnen de functie bar komt hij dan bijv. var foo = "bar" tegen. Op dit moment kijkt hij in welke scope hij zit en registreert hij de variabele foo in die betreffende scope, wat in dit geval de scope van de functie bar is.
De parameter foo van de functie baz, wordt ook in de scope van baz geregistreerd. Let op: foo en bar in de functie baz worden niet in deze fase geregistreerd omdat er geen 'var' voor staat!

Als de declaratiefase gedaan is, begint de initialisatiefase. Deze fase begint weer op regel 1. In deze fase moet je het keyword 'var' wegdenken: er staat dan dus enkel: foo="bar". Stel dus dat je meerdere malen 'var foo="een waarde"'' onder elkaar zet, met verschillende waardes, zullen ze allemaal worden genegeerd, behalve de eerst. De overige worden genegeerd omdat ze niet bestaan tijdens de execution fase.

Tijdens de execution fase is het van belang om te waten wat de Left Hand Side (LHS) en de Right Hand Side (RHS) van een statement is: de linker- en rechterkant van een assignment (als deze er is), zoals bijv. het = teken in regel 1 van de voorbeeldcode. foo is een LHS-reference en RHS is "bar". De LHS is het doel en de RHS is de bron.
Bij de uitvoering (execution fase) van regel 1 wordt aan de scope manager van de global scope (want dit is waarin hij zich nu bevond) gevraagd: "Ik heb een LHS reference voor een variabale die foo heet. Ken je die?". De global scope manager antwoord dan "Ja, die ken ik en hier heb je de referentie naar die variabele zodat jij je operatie verder kan uitvoeren".

Als de functie bar wordt aangeroepen, wordt deze als volgt uitgevoerd:
"Scope van bar, heb jij een referentie voor de variabele foo?".
"Jazeker, hier heb je de referentie en doe je ding ;)"

Als de functie baz wordt aangeroepen, wordt deze als volgt uitgevoerd:
"Scope van baz, heb je een LHS referentie voor de variabele foo? (de parameter)"
"Ja, ik heb hier de definitie, hier is de referentie en doe je ding".
"Scope van baz, heb je een LHS referentie voor variabele bam?"
"Nee, die heb ik niet"
"Okay, dan ga ik een level omhoog. Global scope, heb je een LHS referentie voor een variabale bam?"
"Nee, die heb ik niet, maar ik heb hem meteen aangemaakt voor je. Hier is de referentie en doe je ding".
Omdat er gevraagd wordt om een LHS-referentie wordt er een nieuwe variabele op de global scope aangemaakt als deze niet bestaat. Als er om een RHS-referentie zou worden gevraagd, wordt deze niet aangemaakt.

Voorbeeld uitvoering van code

var foo = "bar";

function bar (){
  var foo = "baz";

  function baz(foo){
    foo = "bam";
    bam = "yay";
  }

  baz();
}

bar();  
foo;    // bar
bam;    // yay
baz();  // error!

Bij het uitvoeren van regel 13:
"Global scope, heb je een RHS referentie voor een variabele bar". Let op: er wordt nu dus om een RHS gevraagd en niet om een LHS. Dit vanwege de doodeenvoudige reden omdat er geen LHS is. Als er geen LHS is, dan gaat het per definitie om een RHS.
De global scope reageert: ""Ja, die heb ik, hier is de referentie."

Als de code op regel 16 wordt uitgevoerd, wordt er aan de globale scope gevraagd naar een RHS variabele genaad baz. Deze bestaat niet en daarom ontstaat er een error. Hierin verschilt een RHS dus van een LHS: als bij een LHS een variabele niet bestaat in de global scope, wordt deze aangemaakt, bij een RHS niet.

Function declaration, function expressions en block scope

Er wordt gesproken over een function declaration als het function keyword het eerste woord is van het statement. Als de functie wordt toegekent aan een variabele, spreekt men over een function expression.
var foo = function bar(){
  var foo = "baz";

  function(baz(foo){
    foo = bar;
    foo;
  }
  baz();
};

foo();
bar();

Regel 1 is dus een (named)function expression en regel 4 een function declaration. De naam van de functie (bar) leeft in zijn eigen scope. Hij is dus niet van buitenaf aan te roepen. Zijn scope loopt van regel 1 t/m regel 9.
Drie nadelen van een anonieme function scope t.o.v. named function expression:

1) Binnen de functie kan je er niet meer naar refereren: bijv. recursie is dan niet mogelijk, het unbinden van een click handler is niet mogelijk omdat je referentie niet meer heb.
2) De naam van de functie wordt in de debug stacktrace getoond. Bij een anonieme functie wordt een vage melding getoond en dit maakt het debuggen lastiger.
3) Een function met een naam is zelfdocumenterend. Het maakt de code begrijpbaarder.

block scope
De catch block van een try...catch block kent een block scope: de variabelen die gedeclareerd zijn in de catch clause, zijn enkel binnen dat block bekend en niet daar buiten.

Lexical scope

De lex uit lexical refereert naar de parsing stage die lexing wordt genoemd en dat gebeurt in de compiler als de code wordt gecompileerd. Lexical scope betekent dus eigenlijk compile-time scope.

Er kan gecheat worden met de lexical scope d.m.v. de eval functie. Met deze functie kan er "geknoeid" worden met variabelen:

var bar = "bar";

function foo(str){
  eval(str); // cheating!
  console.log(bar); // 42
}

foo("var bar = 42;");

Hier wordt dus als het ware code geinjecteerd en in dit geval zorgt dat er voor dat de originele waarde van bar wordt overschreven. Als de JS-engine de functie eval ziet, dan wordt er geen optimalisatie gedaan en dus wordt de code langzamer. Het wordt daarom ook afgeraden deze functie te gebruiken.

Immediately Invoked Function Expression(IIFE)

var foo = "foo";

(function(){
  var foo = "foo2";
  console.log(foo); // "foo2"
})();

console.log(foo); // "foo"

Doel van een IIFY is om dingen te verbergen en daarmee de global scope niet te vervuilen. De function is geschreven tussen twee haakjes om er een expressie van te maken. Anders zou function het eerst woord van een statement zijn en dus een declaration. Een manier om een expression te maken is door er simpelweg niet het eerste woord van te maken. Door de laatste twee haakjes op het eind wordt de expression direct uitgevoerd.
Op regel 4 zit foo dus op de scope van de omringende (anonieme) functie en niet op de global scope.

Let keyword

Met let is de variabele enkel bekend binnen het block waarin het gebruikt wordt:

function foo(){
var bar = "bar";
for(let i=0; i<bar.length; i++){
  console.log(bar.charAt(i));
}

In dit voorbeeld wordt i aan de for loop gekoppeld i.p.v. de functie.

Hoisting

Geen originele term, maar een naam om het principe uit te leggen.

a;
b;
var a = b;
var b = 2;
b;
a;
Deze code wordt door de compiler als volgt verwerkt:
var a;
var b;
a;
b;
a = b;
b = 2;
b;
a;

Het principe dat de declaraties naar boven verplaatst wordt hoisting genoemd: declaraties van variabelen en functies worden naar boven verplaatst.

var a = b();
var c = d();
a;
c;

function b(){
  return c;
}

var d = function(){
  return b();
};

Dit wordt door de compiler intern vertaald naar:

function b(){
  return c;
}
var a;
var c;
var d;
a = b();
c = d();
a; // undefined 
c; // error!
d = function(){
  return b();
};

Als er meerdere functie declaraties zijn dezelfde naam, worden zij overschreven door de laatste declaratie. Als er een variabele is met dezelfde naam, dan gaat deze verloren omdat een functie voorgaat t.o.v. een variabele.

This keyword

Elke functie, als deze wordt uitgevoerd, heeft een referentie naar zijn huidige execution context, genaamd this. De execution context is afhankelijk van hoe de functie wordt aangeroepen en waarvandaan hij is aangeroepen.
Er zijn 4 regels over de manier waarop het this keyword wordt bepaald voor een functie. Allen zijn afhankelijk van de call site: de plaats van de code waar de functie wordt uitgevoerd.

1)
2) Explicit binding rule
Als gebruik wordt gemaakt van call() of apply() in de calling site:
function foo(){
  console.log(this.bar);
}

var bar = "bar1";
var obj = {bar: "bar2" };

foo();  // "bar1"
foo.call(obj); // "bar2". Gebruik obj als referentie voor this


3) Implicit binding rule
In onderstaand voorbeeld: o2.foo() en o3.foo() zijn voorbeelden van implicit bindings. Dit omdat de this nu niet naar de global scope verwijst, maar naar respectievelijk o2 en o3.

De implicit binding rule has an owning or containing context object at the call site

4) Default binding rule
function foo(){
  console.log(this.bar);
}

var bar = "bar1";
var o2 = {bar: "bar2", foo: foo};
var o3 = {bar; "bar3", foo: foo};

foo();// "bar1"
o2.foo(); // "bar2"
o3.foo(); // "bar3"

The default van de this keyword is (in niet strict mode) de global scope (window object in de browser).

Reacties

Populaire posts van deze blog

[SQL Server] varchar vs nvarchar

MS Sql 70-461: Chapter 5

[C#] Class serialiseren en deserialiseren