Scope and Closure in JavaScript
- Scope: where to look for identifiers
- JS organizes scopes with functions and blocks
- JS has a processing phase where a compiler and a scope manager "set up" the code to be executed
- All of the scopes are evaluated/figured out at compile time before the code is executed
Shadowing
- Refers to using the same name for variable declaration in a inner scope.
- See example below, there would now be no lexical way to reference the variable on the first line from within the function:
var teacher = "Kyle";
function theFunction() {
var teacher = "Simpson";
teacher // Simpson
}
teacher // Kyle
Execution
- An identifier (variable, function) can either be a left or a right (destination or a source).
- Think of destination as the destination for a value, assigning a value to something.
- Think of source as the identifier being the source, providing information
var name = "Dempsey"; // name => destination console.log(name); // name => source function myFunc(){return null;} // myFunc => destination myFunc(); // myFunc => source
Lexical Scope
- Think of lexical scope as the compiler stage... Scope is decided and figured out during compile time, not runtime.
- The compiler stage will look for all formal declarations first and sort them into their respective scopes.
- Functions and blocks get their own "sub scope".
- Keywords for declarations are typically
let
,var
,const
,function
andclass
to name a few. - The compiler gets the code ready so execution knows where to look for things.
- When executing, if the current scope doesn't know what you're looking for it will check the parent scope, and continue to look up until it finds what it wants.
- IMPORTANT TO BE AWARE OF: if you get to the global scope and it can't find the variable you want (ie. you're trying to assign an undeclared variable) JS will create the variable in the global scope.
- If you change a global variable in a different scope, it will stay changed unless you create a different variable for that scope.
"strict mode";
will lead to auto global creation resulting in a Reference Error... very useful as we don't want to auto create globals.
Dynamic Scope
- Doesn't exist in JavaScript!
- The runtime conditions affect the scoping.
- The variable assignments are dependant on where the function was called from:
var teacher = "Kyle";
function ask(question) {
console.log(teacher, question);
}
function otherClass() {
var teacher = "Suzy";
ask("Why?"); // Suzy why?
// It doesn't look for the teacher variable lexically from line 56, lexically it'd be Kyle.
// It searches from the where the function was called... Suzy
}
otherClass();
IIFE scoping
- Immediately invoked function expression
- You can surround functions themselves are function calls in parentheses followed by another set of parentheses to get the function and then invoke it.
- The point of it is to give a function its own scope and immediately invoke it.
- It is not a declaration because the first thing in the line isn't function... its
(
, thus making it an expression. - Usually used for a "one and done" function
function myFunc() {
/*do stuff*/
}
(myFunc)()
// OR WE CAN
(function myFunc() {
/*do stuff...*/
}) ();
- Avoid anonymous IIFEs
(function(name) {
console.log(name)
}) ()
// This is anonymous and no good for code readability
(function printName(name) {
console.log(name)
}) ()
// Much better
Block Scoping
- Similar to an IIFE.
- Lighter weight and not Expressions
- Have to use the
let
keyword in a block scope. - Used for encapsulation
var teacher = "Kyle";
{
let teacher = "Suzy"; // Notice the let keyword being used in block scope
console.log(teacher); // Suzy
}
console.log(teacher) // Kyle
let
should only be used when its obviously in a block scope.var
should be used when its going to be used for that entire scope.- a nice way to think is that
let
should be used when it is only needed in that small region (in a local sense), like a in a for loop.
Nested Scope
- Is very important to understand what has access to what.
- Anything has access to everything above it, because when it can't find something it looks up a scope and will continue to do so.
- It doesn't look down though, so you might not have access to scopes when they're nested... see below:
function otherClass() {
var teacher = "suzy";
function ask(question) {
console.log(teacher, question);
}
ask("Why?"); // suzy why?
}
ask("How?") // Reference Error, not in scope, can't look down within scopes, only out.
Function Expression vs Function Declaration
- Function Expressions add their "marble" to their own scope.
- If you make a function expression in global scope, global scope doesn't have access to it
- Function Declarations add a "marble" to the scope it is in.
- If you make a function declaration in global scope it will be available in global scope.
function teacher() {/*...*/}
var myTeacher = function anotherTeacher() {
console.log("Teacher Mumbo Jumbo");
}
console.log(teacher); // teacher function object
console.log(myTeacher); // myTeacher variable holding a function
console.log(anotherTeacher); // Reference Error, not in scope
- Always use Named Function Expressions over Nameless Function Expressions.
- Provides a reliable self-reference (good for recursion).
- More debuggable stack traces .
- More self-documenting code (easier to read and understand). Named Function Declaration > Named Function Expression > Anonymous Function Expressions
Hoisting
- Is a metaphor to discuss lexical scope, it doesn't actually exist.
- Essentially describes how JavaScript looks through scopes during compilation phase.
- It only exists to describe lexical scope in one step, when it fact hoisting isn't real and it happens in 2 steps.
- A way of describing something without getting into the nitty gritty of it.
- Variable hoisting doesn't provide any value other than confusion.
- Function hoisting can provide readability value.
teacher = "Mr. Man";
var teacher; // Isn't really useful ever, only confusing
thatFunction() // Can provide readability having executables at the top
function thatFunction() {
//stuff
}
Closure
- Is when a function is able to remember and access its lexical scope even when the function executes in a different scope.
- Preserves access to variables.
Modules
- Is not a namespace! Below is a namespace.
var workshop = {
teacher: "kyle",
ask(question) {
console.log(this.teacher , question);
},
};
workshop.ask("Is this a module?") // kyle Is this a module?
- A module utilizes encapsulation, which involves having public and private parts, with closure.
- Below is how it is done in ES5:
var workshop = (function Module(teacher) {
var publicAPI = { ask, };
return publicAPI;
// ****************
function ask(question) {
console.log(teacher, question);
}
})("Kyle");
workshop.ask("It's a module, right?"); // Kyle It's a module, right?
-
Modules should involve state changes, if your module doesn't change state, it's an over-engineered namespace.
-
To use modules in ES6 you must use the .mjs file extension.
- This then treats the file as a module, just write everything in the file and export only the functions you want to give access to.
var teacher = "Kyle" export default function ask(question) { console.log(teacher, question); }
-
Modules are file based, can't have a module in a module.
-
Importing can be done like so, going off the export file example above, we want to use ask in a different file:
import ask from "workshop.mjs";
ask("It's a default import");
// "Kyle It's a default import"
import * as workshop from "workshop.mjs"
workshop.ask("Hey"); // Kyle Hey