邹明潮 Notes: YDKJS Scope & Closure

邹明潮
KevinZou
Published in
5 min readMay 19, 2017

Scope

Scope is the set of rules that determines where and how a variable (identifier) can be looked up.

Lexical Scope

Lexical scope means that scope is defined by author-time decisions of where functions are declared. The lexing phase of compilation is essentially able to know where and how all identifiers are declared, and thus predict how they will be looked-up during execution.(Optimization)

  • Bubble 1 encompasses the global scope, and has just one identifier in it: foo.
  • Bubble 2 encompasses the scope of foo, which includes the three identifiers: a, bar and b.
  • Bubble 3 encompasses the scope of bar, and it includes just one identifier: c.

Look-ups

Scope look-up stops once it finds the first match. The same identifier name can be specified at multiple layers of nested scope, which is called “shadowing” (the inner identifier “shadows” the outer identifier). Regardless of shadowing, scope look-up always starts at the innermost scope being executed at the time, and works its way outward/upward until the first match, and stops.

Functions as Scopes

  • Function Declaration
var a = 2;

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

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

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

console.log( a ); // 2

Pitfall: the identifier name foo itself “pollutes” the enclosing scope(global, in this case).

  • Function Expression
var a = 2;

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

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

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

console.log( a ); // 2

Trait: (function foo(){ .. }) as an expression means the identifier foo is found only in the scope where the .. indicates, not in the outer scope. Hiding the name foo inside itself means it does not pollute the enclosing scope unnecessarily.

The best pratice is to always name your function expressions.

Block as Scopes

In Javascript, the variable in the block actually scopes itself to the enclosing scope (function or global).

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

Fortunately, ES6 adds a new keyword let, which attaches the variable declaration to the scope of whatever block it is cotained in.

var foo = true;

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

console.log( bar ); // ReferenceError

In addition to let, ES6 introduces const, which also creates a block-scoped variable, but whose value is fixed.

var foo = true;

if (foo) {
var a = 2;
const b = 3; // block-scoped to the containing `if`

a = 3; // just fine!
b = 4; // error!
}

console.log( a ); // 3
console.log( b ); // ReferenceError!

Garbage Collection

The below code snippet is more likely to make that JS engine will have to keep the structure around, since the click function has a closure over the entire scope (global).

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

var someReallyBigData = { .. }; //global scope

process( someReallyBigData );

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

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

However, Block-scoping can address this concern, making it clearer to the engine that it does not 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");
}, false );

Hoisting

We can be tempted to look at var a = 2; as one statement, but the JavaScript Engine does not see it that way. It sees var a and a = 2 as two separate statements, the first one a compiler-phase task, and the second one an execution-phase task.

What this leads to is that all declarations in a scope, regardless of where they appear, are processed first before the code itself is executed. You can visualize this as declarations (variables and functions) being “moved” to the top of their respective scopes, which we call “hoisting”.

Closure

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

Whatever facility we use to transport an inner function outside of its lexical scope, it will maintain a scope reference to where it was originally declared, and whenever we execute them, that closure will be execised.

Loops and Closure

All five of those functions, though they are defined separately in each loop iteration, are closed over the same share global scope, which has, in fact, only one i in it.

for (var i=1; i<=5; i++) {
setTimeout( function timer(){
console.log( i );
}, i*1000 );
}
Output:
6, 6, 6, 6, 6

Solutions:

Each IIFE has a individual function scope.

for (var i=1; i<=5; i++) {
(function(j){
setTimeout( function timer(){
console.log( j );
}, j*1000 );
})( i );
}

let turns a block into a scope that setTimeout() can close over. Moreover, not only does let in the for-loop header bind the i to the for-loop body, but in fact, it re-binds it to each iteration of the loop, making sure to re-assign it the value from the end of the previous loop iteration.

for (let i=1; i<=5; i++) {
setTimeout( function timer(){
console.log( i );
}, i*1000 );
}

Modules

There are two “requirements” for the module pattern to be exercised:

  1. There must be an outer enclosing function, and it must be invoked at least once (each time creates a new module instance).
  2. The enclosing function must return back at least one inner function, so that this inner function has closure over the private scope, and can access and/or modify that private state.
var MyModules = (function Manager() {
var modules = {};

function define(name, deps, impl) {
for (var i=0; i<deps.length; i++) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply( impl, deps );
}

function get(name) {
return modules[name];
}

return {
define: define,
get: get
};
})(); //Singleton
MyModules.define( "bar", [], function(){
function hello(who) {
return "Let me introduce: " + who;
}

return {
hello: hello
};
} );

MyModules.define( "foo", ["bar"], function(bar){
var hungry = "hippo";

function awesome() {
console.log( bar.hello( hungry ).toUpperCase() );
}

return {
awesome: awesome
};
} );

var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );

console.log(
bar.hello( "hippo" )
); // Let me introduce: hippo

foo.awesome(); // LET ME INTRODUCE: HIPPO

--

--