[Discourse] Paraphrase SCOPE in one time
What is scope
There are 3 steps undergo before executed
It may be self-evident, JavaScript falls under the general category of “dynamic” or “interpreted” languages, it is in fact a compiled language. It is not compiled well in advance nor are the results of compilation portable among various distributed systems. Albeit in more sophisticated ways than we may commonly be aware, but it will undergo roughly typically three steps before it is executed.
-
Tokenizing/Lexing
Breaking up a string of characters into meaningful (to the language) chunks, called tokens. For instance, consider the program var a = 2;. This program would likely be broken up into the following tokens: var, a, =, 2, and ;. Whitespace may or may not be persisted as a token, depending on whether its meaningful or not.
-
Parsing
taking a stream (array) of tokens and turning it into a tree of nested elements, which collectively represent the grammatical structure of the program. This tree is called an “AST” (abstract syntax tree). The tree for var a = 2; might start with a top-level node called VariableDeclaration, with a child node called Identifier (whose value is a), and another child called AssignmentExpression, which itself has a child called NumericLiteral (whose value is 2).
-
Code-Generation
The process of taking an AST and turning it into executable code. This part varies greatly depending on the language, the platform it’s targeting, and so on. So, rather than get mired in details, we’ll just handwave and say that there’s a way to take our previously described AST for var a = 2; and turn it into a set of machine instructions to actually create a variable called a (including reserving memory, etc.), and then store a value into a.
For JavaScript, the compilation that occurs happens, in many cases, mere microseconds (or less!) before the code is executed. Any snippet of JavaScript has to be compiled before (usually right before!) it’s executed.
lexical scope
Lexical scope is based on where variables and blocks of scope are authored, by you, at write time, and thus is (mostly) set in stone by the time the lexer processes your code.
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.
The lexical scope look-up process only applies to first-class identifiers, such as the a, b, and c. If you had a reference to foo.bar.baz in a piece of code, the lexical scope look-up would apply to finding the foo identifier, but once it locates that variable, object property-access rules take over to resolve the bar and baz properties, respectively.
- firstly, go innermost
- then foremost
- next to the outermost
Lexical can be cheated
How could there possibly be a way to “modify” (a.k.a., cheat) lexical scope at runtime? The answer is yes and the following two mechanisms are frowned upon in the wider community as bad practices to use in your code.
- eval()
The eval(..) function can programatically generate code inside of your authored code, and run the generated code as if it had been there at author time.
eval(..) when used in a strict-mode program operates in its own lexical scope, which means declarations made inside of the eval() do not actually modify the enclosing scope.
function foo(str, a) {
// When strict model, the result will be "1, 2"
// "use strict"
eval( str ); // cheating!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3
-
with()
with()
is typically explained as a shorthand for making multiple property references against an object without repeating the object reference itself each time.
There’s much more going on here than just a convenient shorthand for object property access
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2—Oops, leaked global!
When we pass in o2, since it does not have an a property, no such property is created, and o2.a remains undefined
.
But then we note a peculiar side-effect, the fact that a global variable a was created by the a = 2 assignment.
with()
essentially creates a whole new lexical scope (again, at run‐time) by treating an object reference as a scope and that object’s properties as scoped identifiers.
Performance
To the performance interest, the conclusion will boil down(煮沸,归结为) to the below wrap-up.
The downside to these mechanisms is that it defeats the engine’s ability to perform compile-time optimizations regarding scope look-up, because the engine has to assume pessimistically that such optimizations will be invalid. there’s no getting around the fact that without the optimizations, code runs slower.
Collision Avoidance
function foo() {
function bar(a) {
i = 3; // changing the i in the enclosing scope's
// for-loop
console.log( a + i );
}
for (var i=0; i < 10; i++) {
bar( i * 2 ); // oops, inifinite loop ahead!
}
}
foo();
function declaration and function expression
The key difference we can observe here between a function declaration and a function expression relates to where its name is bound as an identifier. The name of normal function is bound to the enclosing scope, it's a function declaration; but the immediate invoking function is not bound to the enclosing scope then it's a function expression.
Function expressions can be anonymous, but function declarations cannot omit the name. We tend to encourage this idiomatic style of code(With a name).
They have several drawbacks to consider if a functioin without a name:
- Anonymous functions have no useful name to display in stack traces, which can make debugging more difficult.
- Without a name, if the function needs to refer to itself, for recursion, etc., the deprecated arguments.callee reference is unfortunately required. Another example of needing to self-reference is when an event handler function wants to unbind itself after it fires.
- Anonymous functions omit a name, which is often helpful in providing more readable/understandable code. A descriptive name helps self-document the code in question.
IIFE(immediately invoked function expression)
A variety of IIFE variation.
var a = 2;
(function IIFE(){
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
(function(){
console.log(123);
}())
The 2 above forms are purely a stylistic choice which you prefer.
var a = 2;
(function IIFE( global ){
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
})( window );
console.log( a ); // 2
We have a clear stylistic delineation for global versus nonglobal references.
undefined = true; // setting a land-mine for other code! avoid!
(function IIFE( undefined ){
var a;
if (a === undefined) {
console.log( "Undefined is safe here!" );
}
})();
This pattern addresses the (minor niche) concern that the default undefined identifier might have its value incorrectly overwritten, causing unexpected results.
var a = 2;
(function IIFE( def ){
def( window );
})(function def( global ){
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
});
Some people find it a little cleaner to understand, though it is slightly more verbose.
JavaScript only has some strange niche behaviors that expose block scope functionality.
Like for
and if
do not have block scope facility. However, with
and try/catch
have the block scope.
let
let
sits alongside var as another way to declare variables.
The let keyword attaches the variable declaration to the scope of whatever block (commonly a { .. } pair) it’s contained in. In other words, let implicitly hijacks any block’s scope for its variable declaration.