Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
769 lines (504 sloc) 27.2 KB

js engine

in traditional compiled language process, your program goes through 3 steps BEFORE executed (this is its compilation):

  1. tokenizing/lexing - breaking up characters into meaningful language chunks EX: var a = 2; becomes var, a, =, 2, and ;.

  2. Parsing: taking an array of tokens and turning it into a tree of nested elemnts, which collectively represent the grammatical structure of a program. This tree is called an AST (abstract syntax tree.)

  3. Code Generation: the process of taking an ASt and turning it into executable code.

JS engine is way more complex than just these 3 steps, as are others.

JS engines dont get the luxury of having plenty of time to optimize bc JS compilation doesnt happen in a build step ahead of time like some other languages.

in JS, compilation occurs in many cases, microseconds before the code is executed.

For simplicity's sake, lets just say that any snipped of JS has to be compiled usually right before its executed. so it'll take var a = 2;, comiple it and be ready to execute right away.

Thinking about Scope:

lets think about scope like a conversation...here's our characters:

  1. engine - responsibel for the start-to-finish process
  2. compiler - one of 'engine's friends; handles all the dirty work of parsing and code Generation
  3. Scope - another one of engine's friends; collects and maintains a lookup list of all the declared variables, and enforces a strict set of rules as to how these are accessible to currently executing code.

variable assignment: what actually happens.

  • 1st, the compiler declares a var if not prev declared in the current scope, then when executing, 'engine' looks up the var in 'Scope' and assigns to it, if found.

oh but it gets way deeper...when 'engine' executes code that 'compiler' produced, it has to look up variable a to see if its been declared..this is called 'consulting scope'. but THE TYPE OF LOOKUP 'engine' performs affects the outcome of that lookup...

2 diff types of lookups LHS - left hand side

RHS - right hand side

a LHS lookup is done when a var appears on the left-hand side of an assignment operation, and an RHS for vice-versa.

  • A RHS lookup is indistinguishable from simply a look-up of the value of some var, whereas the LHS is trying to find the variable container itself so that it can assign.

lets understand RHS a bit better, because im confused...

console.log( a );

^ this is a reference to 'a' bc nothing is being assigned to 'a' here. instead we just want to retrieve the vlaue of 'a'.

look at this in contrast:

a = 2;

^ the ref to 'a' here is a LHS ref, bc we dont actually care what the current alue is, we just want to find the variable as a target to the = 2 assignment operation.

CHEATSHEET: LHS = "whos the target of the assignment" : a = 2; RHS = "whos the source of the assignment" : console.log(a);

Nested Scope

so 'Scope' is a set of rules for looking up variables by their identifier name, but theres usually more than one 'scope' to consider.

scopes are nested inside other scopes. if a var cant be found in the immediate scope, 'Engine' consults the next outer containing scope, continuing until found or the outmost (aka global) scope has been reached.

Errors

Why does it matter whether we call it LHS or RHS?

  • because these 2 lookups behave differently in the circumstance where the var hasnt been declared (is not found in any consulted 'Scope').

EX:

function foo(a) {
	console.log( a + b );
	b = a;
}

foo( 2 );

when the RHS look-up occurs for 'b' for the 1st time, it wont be found. This is said to be an 'undeclared' var, because its not found in the scope. If an RHS look-up fails to ever find a var, anywhere in the nested scope, this reuslts in a ReferenceError being thrown by the 'engine'. Its important to noe that the error is of the type ReferenceError.

In constrast, if its an LHS lookup and arrives at the global scope without finding the var (and isnt in 'strict mode', it'll create one on the global scope and hand it back to 'engine'.

STRICT MODE: added in ES5, has a number of diff behaviors from normal mode. One is that is disallows the automatic/implicit global var creation. In that case, there'd be no var and a ReferenceError would be called.

now if the var for an RHS lookup is found but you're trying to do something with its value that is impossible like trying to execute a fn as a non fn-value or reference a property on a null or undefined value, then 'engine' throws a diff kind of error, called a TypeError.

TO RECAP: ReferenceError: scope resolution failure. TypeError: implies that Scope resolution was successful, but there was an illegal action attempted against the result

Ch.2 Lexical Scope

Theres 2 models for how scope works:

  1. Lexical Scope (used by most programming Languages)
  2. Dynamic Scope

Lexing = tokenizing. As you recall from the last chapter, lexing is the process of taking characters and attaching semantic meaning to them.

Lexical Scope is scope that is defind at lexing time. so the lexer basically sets your scope in stone.

note: scope look-up stops once it find its first match.

No matter where a fn is invoked from or how its invoked, its lexical scope is ONLY defined by where the fn was declared.

Cheating Lexical Scope

  • this LEADS TO POORER PERFORMANCE. before we chat about that though, lets look at these 2 examples:
eval

eval() fn in js takes a string as an arg & treats the contents of the string as if it had actually been authored code at that point in the program. in other words, you can programmatically generate code inside of your authored code, and run the generated code as if it had been there at author time....whaaa?

^ in this light, eval() allows you to modify the lexical scope in environment by cheating and pretending that author-time (aka lexical) code was there all along.

^ im not getting this. :(

look at this example:

function foo(str, a) {
	eval( str ); // cheating!
	console.log( a, b );
}

var b = 2;

foo( "var b = 3;", 1 ); // 1 3

this results in '1 3' instead of '1 2' because eval() cheats acts as if b = 3 was there the whole time, and lexical scope doesnt weird out about it because it doesnt know, and it first sees b as 3 so it stops there.

Performance with eval()

  • eval() cheats an otherwise author-time defined lexical scope by modifying or creating new lexical scope at runtime. So whats the big deal?

answer: The JS engine has a number of performance optimizations that it performs during the compilation phase. some of these boil down to just being able to statically analyze the code as it lexes, and pre-detemine where all the var and fn declarations are, so it takes less effort to resolve identifiers during execution. But, if 'engine' finds an eval() in the code, it has to assume that everything it knows about id location may be invalid.

TL;DR -- most of the optimizations that 'engine' would run would be pointless if eval() is present because the ground truth could possibly change, so it just doesnt perform those optimzations at all. wow.

so your code will definitely run slower by including eval()

Ch.3: Function vs Block Scope

-as in ch2, scope is basically a bunch of bubbles in which identifers (vars, fns) are declared. but what exatly makes a new bubble? is it only a fn? can other structures in js create bubbles of scope?

simple answer: only fns, no other structures create scope bubbles harder answer: not so fast....huh? <-- idk

  • fn scope encourages the idea that all vars belong to the fn, and can be used and reused throughout the entirety of the fn. This design approach can be useful and totally make use of the 'dynamic' nature of JS vars to take on values of different types as needed. BUT, if you dont take precautions, variables existing across the entirety of a scope can lead to some unexpected pitfalls.
Hiding in plain scope
  • take a section of code you've written, wrap it in a fn to 'hide' the code. creating a scope bubble around the code in question means that any declarations in that code will now be tied to the scope of the new wrapping fn, rather than the previously enclosing scope. TLDR: you can 'hide' vars and fns by enclosing them in the scope of a fn.

^ what makes this useful?

  • software design principle 'principle of least privilege': in the design of software, like the API for a module/object, you should only expose what is minimally necessary, and 'hide' everything else.

  • another benefit of 'hiding' is to avoid unintended collision b/w 2 diff identifiers with the same name but different intended usages.

Functions as scopes.

check out this example:

var a = 2;

function foo() { // <-- insert this

	var a = 3;
	console.log( a ); // 3

} // <-- and this
foo(); // <-- and this

console.log( a ); // 2

now this works for creating a new scope, but isnt ideal beause now the fn name foo() pollutes the global namespace and we also have to explicitly call the fn so the wrapped code executes...we can wrap it in an IIFE so it'll run automatically and we dont have to have an unncessary fn:

var a = 2;

(function foo(){ // <-- insert this

	var a = 3;
	console.log( a ); // 3

})(); // <-- and this

console.log( a ); // 2
Anonymous vs. named functions
  1. anon fns have no useful name in stack traces, makes debugging more difficult.
  2. without a name if the fn has to refer to itself (for recursion, etc) the deprecated arguments.callee reference is required.
  3. anon fns mean no descriptive name about the code to self-document.
Blocks as Scopes

fns are the most common unit of scope, and def. the most wide-spread, other units of scope are posible and the usage of these other scope units to lead to better, cleaner code.

EX:

for (var i=0; i<10; i++) {
	console.log( i );
}

we declare i directly inside the for-loop head, most likely bc our intent is to only use i within the context of the for-loop. This is what block scoping is about: declaring variables as close as possible, as local as possible, to where they will be used.

another ex:

var foo = true;

if (foo) {
	var bar = foo * 2;
	bar = something( bar );
	console.log( bar );
}

BLOCK SCOPE is a tool to extend the earlier 'principle of least privilege exposure' from hiding info in fns to hiding info in blocks of our code. so considering that for-loop example, why pollute the entire scope of a fn with i var that is only going to be used for the for-loop?

devs may prefer to CHECK themselves against accidentally reusing variables outside of their intended purpose, such as being issue an error about an unknown variable if you try to use it in the wrong place. Block scoping (IF IT WERE POSSIBLE) for the i variable would make i only available for the for loop, causing an error if i is accessed elsewhere in the fn. This helps to ensure that vars arent re-used in hard to maintain ways.

on the surface, JS has no facility for block scope. but lets dig a bit deeper:

Try/Catch:

  • Its a very little known fact that JS in ES3 specified the var declaration in the catch clause of a try/catch statement to be block-scoped to the catch block.
try {
	undefined(); // illegal operation to force an exception!
}
catch (err) {
	console.log( err ); // works!
}

console.log( err ); // ReferenceError: `err` not found

As you can see, err exists only in the catch clause, and throws an error when you try to reference it elsewhere.

Let:

  • the let keyword attached variable declaration to the scope of whatever block ({...}) its contained in. let implicitly hijacks any block's scope for its own variable declaration:
var foo = true;

if (foo) {
	let bar = foo * 2;
	bar = something( bar );
	console.log( bar );
}

console.log( bar ); // ReferenceError

Using let to attach a variable to an exising block is somewhat implicit.

Garbage Collection:

  • another reason block-scoping is useful relates to closure and garbage collections to reclaim memory...we'll briefly illustrate here, but the closure mechanism is explained in detail in ch. 5.

Take a look at this example:

function process(data) {
	// do something interesting
}

var someReallyBigData = { .. };

process( someReallyBigData );

var btn = document.getElementById( "my_button" );

btn.addEventListener( "click", function click(evt){
	console.log("button clicked");
}, /*capturingPhase=*/false );

the 'click' evt handler doesnt need someReallyBigData var at all. This means after process(), the big memory-heavy data structure could be garbage collected. however its quite likely that the jS engine will still have to keep the structure around, since the click fn has a closure over the entire scope.

Block-scoping can address this concern, making it clearer to the engine that it doesnt need to keep someReallyBigData around:

function process(data) {
	// do something interesting
}

// anything declared inside this block can go away after!
{
	let someReallyBigData = { .. };

	process( someReallyBigData );
}

var btn = document.getElementById( "my_button" );

btn.addEventListener( "click", function click(evt){
	console.log("button clicked");
}, /*caturingPhase=*/false );

^ NICE!!!!! THIS IS SUPER IMPORTANT!!!! Declaring explicit blocks for variables to locally bind to it a powerful tool that you can add to your toolbox

Const:

  • const creates a block-scoped variable, but a fixed value.

Review: -fns are the most common unit of scope in js. Vars and fns declared inside another fn are essentially 'hidden' from any of the enclosing 'scopes' whch is an intentional design principle of good software.

  • fns arent the only unit of scope. Block scope refers to the idea that vars and fns can belong to an arbitrary block (generally, any {...} pair) of code, rather than only to be the enclosing function.

  • try/catch structure has block-scope in the catch clause.

  • let allows declarations of vars in any abitrary block of code.

  • block scope shouldnt be taken as an outright replacement of standard fn scope. Both functionalities co-exist, and devs should use both where appropriate.

[^note-leastprivilege]: Principle of Least Privilege

Ch. 4: Hoisting

Theres a temptation to think all code in JS in procedural, left-to-right and top-bottom. thats substantially true but theres one part of that assumption than lead to incorrect thinking about your program:

EX:

a = 2;

var a;

console.log( a );

many ppl would say its undefined but its actually 2

another ex:

console.log( a );

var a = 2;

^ will output undefined. SO WHAT COMES FIRST? THE CHICKEN (the declaration) OR THE EGG (the assignment).

To answer, lets look back to our earlier ch.1 discussion of compilers. Rememeber 'engine' will actually compile the JS before it interprets it, and part of the compilation phase was to find and associate all declarations with their appropriate scopes. Ch2 showed us that this is the heart of Lexical Scope.

The best way to think about things is that all declarations, both vars and fns, are processed first, before any part of your code is executed.

When you see var a = 2;, you probably think of that as one statement. But JavaScript actually thinks of it as two statements: var a; and a = 2;. The first statement, the declaration, is processed during the compilation phase. The second statement, the assignment, is left in place for the execution phase.

Our first snippet then should be thought of as being handled like this:

var a;
a = 2;

console.log( a );

One way of thinking about this process is that var and fn declarations (calls) are 'moved' from where they appar in the flow of the code to the top of the code. This gives rise to the name 'hoisting'.

SO THE EGG (the declaration) COMES BEFORE THE ASSIGMENT (assigment).

foo();

function foo() {
	console.log( a ); // undefined

	var a = 2;
}

the fn foos declaration is hoisted such that the call on the first line is able to execute.

  • Its important to note that hoisting is PER SCOPE. so var a is hoisted to the top of the scope of foo.

the actual program could be interepreted as this:

function foo() {
	var a;

	console.log( a ); // undefined

	a = 2;
}

foo();

GOOD TO KNOW/REMEMBER: only the declarations themselves are hoisted, while any assignments or other executable logical are LEFT IN PLACE. if this appears confusing, just look at the example above where the program is intepreted a = 2 on the final line of the block, with the console.log() statement rendering undefined.

ALSO: Function declarations are hoisted, but function expressions are not.

foo(); // not ReferenceError, but TypeError!

var foo = function bar() {
	// ...
};

^ the variable indentifier foo is hoisted and attached to the enclosing global scope of this program, so foo() doesnt fail as a referenceError. but foo have no value yet (as it would if it had been a true fn declaration instead of an expression). so foo() is attempting to invoke the undefined value, which is a TypeError illegal operation.

NOTE TO SELF: tell me, whats the difference between a function expression and a function declaration? answer: a function expression executes something and a function declaration declares (gives birth) to a function to be called/executed later.

Functions First

both function declarations and var declarations are hoisted. But a subtle details is that FUNCTIONS ARE HOISTED FIRST, THEN VARIABLES.

foo(); // 1

var foo;

function foo() {
	console.log( 1 );
}

foo = function() {
	console.log( 2 );
};

1 is printed instead of 2 because the function declaration is hoisted up first. 'Engine' interprets like this:

function foo() {
	console.log( 1 );
}

foo(); // 1

foo = function() {
	console.log( 2 );
};

Good to know: function declarations that appear inside of normal blocks typically hoist to the enclosing scope rather than being conditional as this code implies:

foo(); // "b"

var a = true;
if (a) {
   function foo() { console.log( "a" ); }
}
else {
   function foo() { console.log( "b" ); }
}

^ since the if statement is hoisted up, it doesnt know a = true and therefore runs the else block.

as you see, this isnt super reliable even though the code reads otherwise...try to avoid declaring functions in blocks.

Review

  • we look at var a = 2 as one statement but 'engine' sees 2: var a & a=2, the first one a compiled-phase task, and the second one an execution-phase task

  • what this leadsd to is that all declarations in a scope, regardless of where they appear, are processed FIRST before the code itself is executed. so vars and fns get moved to the top of their respective scopes, and this is called 'hoisting'.

  • declarations themselves are hoisted, but assignments, even assignments of function expressions, ARE NOT hoisted.

Ch.5: Scope Closure

Closure is all around you in JS, just recognize and embrace it. They already exist, you just need to the mental context to see and leverage them for yourself.

Closure = when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.

EX:

function foo() {
	var a = 2;

	function bar() {
		console.log( a );
	}

	return bar;
}

var baz = foo();

baz(); // 2 -- Whoa, closure was just observed, man.

^ as you see, bar() is executed, but its executed OUTSIDE of its declared lexical scope.

lets break this down a bit....after foo() is executed, normally we'd expect that the entirety of the inner scope of foo() would go away, because we know 'engine' employs a 'garbage collector' that comes along and frees up memory once its no longer in use. since it'd appear that the contents of foo() are no longer in use, it'd seem natural that they should be considered gone. BUT closures dont let this happen: that inner scope is in fact still in use and so it doesnt go away. the function bar() is using that scope.

By virtue of where it was declared, bar() has a lexical scope closure over that inner scope of foo(), which keeps that scope alive for bar() to reference at any later time.

bar() still has a reference to the scope of foo(), and that reference is called CLOSURE.

^ Could you say that when a function has a reference to the scope of another function thats closure?

The function is being invoked well outside of its author-time lexical scope. Closure lets the fn continue to access the lexical scope it was defined in at author-time.

interesting: any of the ways the fns can be passed around as values and invoked in other locations are all examples of observing/exercising closure.

another example:

function foo() {
	var a = 2;

	function baz() {
		console.log( a ); // 2
	}

	bar( baz );
}

function bar(fn) {
	fn(); // look ma, I saw closure!
}

we pass baz() over to bar() call that inner function (labeled fn) and when we do, its closure over the inner scope of foo() is observed by accessing the variable a.

Another example of passing-around of functions indirectly, but still seeing closure:

var fn;

function foo() {
	var a = 2;

	function baz() {
		console.log( a );
	}

	fn = baz; // assign `baz` to global variable
}

function bar() {
	fn(); // look ma, I saw closure!
}

foo();

bar(); // 2

interesting: whatever facility we use to transport an inner fn outside of its lexical scope, it will maintain a scope reference to where it was originally declared, and wherever we execute it, that closure will be exercised.

Closure could be redefined to me as: "you can take the boy out the trailer park but you cant take the trailer park out the boy." wherever the fn was created and given semantic meaning in the lexing process, it has access to that scope, regardless where it is invoked.

Closure in the real world

weve looked at academic exercises, lets see something a bit more practical.

function wait(message) {

	setTimeout( function timer(){
		console.log( message );
	}, 1000 );

}

wait( "Hello, closure!" );

^ timer() has a scope closure over the scope of wait(), keeping and using a ref to the variable message.

lets take a look at this jQuery example:

function setupBot(name,selector) {
	$( selector ).click( function activator(){
		console.log( "Activating: " + name );
	} );
}

setupBot( "Closure Bot 1", "#bot_1" );
setupBot( "Closure Bot 2", "#bot_2" );

so we have a fn within a click fn called activator which has a closure over setupBot.

SO...whenever and whereever you treat functions (which access their own respective lexical scopes) as first-class values and pass them around, you are likely to see those functions exercising closure. example of these are timers, event handlers, AJAX requests, etc...when you pass in a callback fn, be ready to see closure occur.

Modules

other code patterns leverage the power of closure but dont appear on the surface to be about callbacks. let's examine the most powerful of them: the MODULE!

function foo() {
	var something = "cool";
	var another = [1, 2, 3];

	function doSomething() {
		console.log( something );
	}

	function doAnother() {
		console.log( another.join( " ! " ) );
	}
}

^ doesnt look like any closure, just a fn with some vars and fns in it which do indeed have lexical scope (and thus closure) over their containing fn, foo().

but now look at this:

function CoolModule() {
	var something = "cool";
	var another = [1, 2, 3];

	function doSomething() {
		console.log( something );
	}

	function doAnother() {
		console.log( another.join( " ! " ) );
	}

	return {
		doSomething: doSomething,
		doAnother: doAnother
	};
}

var foo = CoolModule();

foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

^ this is a module, and this is what we call the "Revealing Module Pattern", where we only return the necessary things via an object. you see that the variables are hidden, and coolModule's functionalities are readily available through the returned object.

important: we have to create var foo = coolModule(); first because the fn has to be invoked for an instance to be created. without the execution of the outer fn, the creation of the inner scope and the closures wouldnt occur.

You can think of the reutnred object as a PUBLIC API for our module.

NOTE: you dont HAVE to return an object from our module...you could just return back an inner fn directly (ex: return blah). jQuery is actually a good example of this...$ identifiers are the public API for the jQuery module, but they are themselves just a fn (which can have properties, since all functions are objects anyway).

the doSomething() and doAnother() fns have closure over the inner scope of the module 'instance' (arrived at by first invoking coolModule()). When we transport those functions outside of the lexical scope, by way of property references on the object we return, we have now set up a condition by which closure can be observed and exercised.

TLDR: 2 requirements for the module pattern to be exercised...

  1. there must be an outer enclosing fn, invoked at least once to create a new module instance.
  2. the enclosing fn must return at least 1 inner fn, so that this inner fn has closure over the private scope, and can access/modify that private state.

now from the coolModule example we've seen above, we can invoke this as many times as we possibly want. you can also have a singleton instance as well:

var foo = (function CoolModule() {
	var something = "cool";
	var another = [1, 2, 3];

	function doSomething() {
		console.log( something );
	}

	function doAnother() {
		console.log( another.join( " ! " ) );
	}

	return {
		doSomething: doSomething,
		doAnother: doAnother
	};
})();

foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

ALSO Modules are just fns so they can receive parameters:

function CoolModule(id) {
	function identify() {
		console.log( id );
	}

	return {
		identify: identify
	};
}

var foo1 = CoolModule( "foo 1" );
var foo2 = CoolModule( "foo 2" );

foo1.identify(); // "foo 1"
foo2.identify(); // "foo 2"

another slight (but powerful) variation on the module pattern is to name the object you're returning as your public API:

var foo = (function CoolModule(id) {
	function change() {
		// modifying the public API
		publicAPI.identify = identify2;
	}

	function identify1() {
		console.log( id );
	}

	function identify2() {
		console.log( id.toUpperCase() );
	}

	var publicAPI = {
		change: change,
		identify: identify1
	};

	return publicAPI;
})( "foo module" );

foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE

^ notice here that inside your inner scope, you can modify the object to be returned FROM THE INSIDE!

Review

Closure is just a standard and just a fact of how we write code in a lexically scope environment, where fns are values and can be passed around at will.

CLOSURE = when a fn can remember and access its lexical scope even when its invoked outside its lexical scope. CLOSURE = you can take the boy out the trailer park, but you cant take the trailer park out the boy.

modules require 2 characteristics:

  1. an outer wrapping fn being invoked to create the enclosing scope
  2. the return value of the wrapping fn must include a reference to at least one inner fn that then has closure over the private inner scope of the wrapper.