What is the scope
Compilation principle
JavaScript is a compiled language, but unlike traditional compiled languages, it is not pre-compiled and the compiled results cannot be portable across distributed systems. In traditional compiled languages, compilation takes three steps.
Word Segmentation/Lexical Analysis (Tokenizing/ Lexing)
This process breaks the string into meaningful code blocks, such asvar a = 2;
Broken down intoVar, a, = 2;
.Parsing/parsing
Flow the lexical unit toAbstract Syntax Tree (AST)
Code generation
The process of turning an abstract syntax tree into executable code is called code generation, and regardless of the details, it is called code generationvar a = 2;
This code becomes a machine-recognized machine instruction that creates a code nameda
And stores a value ina
In the.
JavaScript compilation is naturally much more complex than this, with steps for code optimization during lexical analysis and code generation.
Understanding scope
Let’s start with three concepts
engine
Responsible for JavaScript compilation and execution from start to finishThe compiler
Responsible for syntax analysis and code generationscope
Responsible for collecting and maintaining a series of queries made up of all declared identifiers (variables) and enforcing a very strict set of rules that determine access to these identifiers by currently executing code.
Assignment to a variable performs two actions: first, the compiler declares a variable in the current scope (if it hasn’t been declared before), and then at runtime the engine looks for the variable in the scope and assigns it if it can find it.
LHS and RHS
The compiler generates code, and when the engine executes it, it looks for variables to determine whether they are declared, but how it looks affects the result.
LHS
Variables that appear on the left side of an assignment are called LHS, for examplea=2
RHS
Variables that appear on the right side of an assignment are called RHS, for examplea=b
In theb
.
The above is just a simple understanding. To be more precise, an LHS query is an attempt to fetch the container of variables itself, while an RHS query is an attempt to fetch the value stored in the variable. Assignment does not mean =. Take a chestnut
function foo(a) {
console.log(a)
}
foo(2)
Copy the code
The last line foo(2) is an RHS query on function foo. There is also a hidden LHS, which is that a=2 is passed to function foo. Console. log is also an RHS on the console object, and a in console.log(a) is also an RHS.
Scope nesting
function foo(a) {
return a+b
}
var b = 2
foo(1)
Copy the code
In the above case, we have two scopes, the function scope created by the foo function and the global scope, which form a nested scope. The global scope wraps around the Foo scope. When b cannot be queried by RHS within a function, the engine looks up the scope (global scope) and finds the b variable in the global scope.
So traversing the scope chain is simple: if you can’t find a variable in the current scope, look up the next level of scope, and stop looking until you can’t find the global scope.
abnormal
Why do we need to understand this? In non-strict mode, LHS will automatically declare a global variable if the variable has not been declared, while RHS will raise Uncaught ReferenceError: Uncaught ReferenceError: A is not defined. In strict mode, both queries will generate an error.
ReferenceError is a scoping failure, meaning that a variable cannot be found, and TypeError is a misreference to a variable, for example
let a = 'a'
a() // TypeError
Copy the code
Lexical scope
Scope is generally divided into two types, lexical scope and dynamic scope. JavaScript uses lexical scope, which means that the scope is determined by the position of the variable function declaration when writing code. Compile time basically knows where and how all identifiers are declared, so you can predict how they will be looked up during execution.
JavaScript has two mechanisms for “cheating” lexical scopes.
eval()
function foo(str, a) {
eval(str)
console.log(a,b)
}
var b = 2
foo("var b = 3;".1) // print 1,3 instead of 1,2
Copy the code
However, it is important to note that in strict mode, the eval function has its own scope, so it does not affect the scope. There are others like setTimeout and setInerval. The first argument can be a string, which can be parsed as dynamically generated function code, but it is outdated and should not be used. New Function() is similar.
with
With is often used as a shortcut to refer repeatedly to multiple properties in the same object.
var obj = {
a: 1.b: 2.c: 3
}
// Copy again
obj.a = 2
obj.b = 3
obj.c = 4
/ / with
with (obj) {
a = 2;
b = 3;
c = 4
}
Copy the code
So how does with trick the scope?
function foo(obj) {
with(obj) {
a = 2}}var obj1 = { a: 1 }
var obj2 = { b: 2 }
foo(obj1)
console.log(obj1.a) / / 2
foo(obj2)
console.log(obj2.a) //undefined
console.log(a) //2 -- a is leaked into the global scope
Copy the code
When obj1 is passed to with, it uses the scope of obj1, which has an A attribute, whereas obj2 is passed to with, which has no A attribute, so it performs a normal LHS declaration declaring a global variable a.
performance
Because neither engine can optimize scoped lookups at compile time, the engine can only assume that such optimizations are ineffective and cause code to run slowly, so don’t use them, just learn from them.
Function scope, block scope
Function scope
A function scope is a variable created when a function is defined that is only inside the function and cannot be accessed directly outside the function.
function foo() {
var a = 'a'
function bar() {
var b = 'b'}}console.log(a) / / an error
console.log(b) / / an error
bar() / / an error
Copy the code
We can use this function scope to hide some of the internal implementation. There is a principle of least privilege in software design, also known as the principle of least authorization or least exposure, which means that in software design, the minimum necessary content should be exposed, and other content should be hidden, such as module or object API design. Take a chestnut
function doSomething(a) {
const b = a + doSomethingElse(a*2)
return b
}
function doSomethingElse(b) {
return b - 1
}
const c = doSomething(2)
Copy the code
We can change the following code according to the principle of least privilege
function doSomething(a) {
function doSomethingElse(b) {
return b - 1
}
return a + doSomethingElse(a*2)}Copy the code
DoSomethingElse functions are internal implementations that should never be accessed by the outside world. This is not necessary and can be risky, so we should hide them, simply exposing doSomething to the outside world is sufficient, both functional and implementable, and has an important advantage. Is to avoid naming conflicts. A typical example is importing multiple packages in a global scope. A special and unique variable, usually an object, is declared in the global scope, and all identifiers in the library are accessed through the attributes of the variable, rather than all identifiers being exposed to the global variable.
Anonymous and named functions
Functions are divided into function declarations and function expressions. The function keyword appears in the first word of the declaration, then it is a function declaration, otherwise it is a function expression.
// Function declaration
function foo() {}
// Function expression
const bar = function() {}
Copy the code
For function expressions, and subdivided into named functions and anonymous functions, for anonymous functions, the most seen is the callback function inside.
setTimeout(function() {
console.log('are you ok? ')},1000)
Copy the code
Because the function ()… There is no name identifier, so this is an anonymous function expression. Function declarations must have names, otherwise they are invalid. Anonymous function expressions have several disadvantages that need to be considered carefully.
- Anonymous functions do not show meaningful function names on the stack trace, so debugging is difficult.
- Cannot reference itself. Unless it’s out of date
arguments.callee
- Omitting the name makes the code less readable.
The opposite is a named function, such as const a = function foo() {}
Execute function immediately
var a = 2
(function() {
var a = 3
console.log(a) / / 3}) ()console.log(a) / / 2
Copy the code
Because the function is wrapped in (), it is a function expression rather than a function declaration, and adding a () to the end executes the function immediately.
Block scope
For, if, with, and try/catch can create a block scope.
for (var a = 0; i < 10; i++) {... }if(a) {... }with(obj){... }try {
undefined(a)// force an error into catch execution
} catch(err) {... }console.log(err) // ReferenceError
Copy the code
You can also create a block scope directly with a brace
{
let a = 'a'
}
Copy the code
Let and const
Because variables declared by lets and const are bound to their respective scopes. A good example is the let loop
for (var i = 0; i < 10; i++) {
setTimeout(() = > {
console.log(i)
})
}
// Output 10 10s
for (let i = 0; i < 10; i++) {
setTimeout(() = > {
console.log(i)
})
}
// Outputs 0 to 10
Copy the code
Because a new scope is bound with each iteration, each I is a new I bound to a different block scope that is not common.
Variable ascension
General intuition tells us that the code will execute line by line from top to bottom. But that’s not exactly true.
a = 2
var a;
console.log(a) / / 2
// Another example
console.log(a) //undefined
var a = 2
Copy the code
The first part of the code above prints 2 instead of undefined, and the second part prints undefined because the variable was raised when it was declared.
Recall that part of the compiler phase is finding all the declarations and correlating them to scopes. var a = 2; JavaScript will view this code as two parts, with the first part var a being compiled and the second part a = 2 being executed. So the first example above can be viewed as the following code
var a
a = 2
console.log(a) / / 2
Copy the code
The second piece of code is
var a
console.log(a) // undefined
a = 2
Copy the code
Function declarations also promote variables
foo() / / 666
function foo() { console.log(Awesome!)}Copy the code
But function expressions don’t
foo() //TypeError
var foo = function() { console.log(777)}Copy the code
Because this is how the code actually runs
var foo
foo() // This is an error
foo = function() { console.log(777)}Copy the code
Function is preferred
Both function declarations and variable declarations promote variables, but functions are promoted first.
foo() // Output 1 instead of 2
var foo
function foo() { console.log(1) }
foo = function() { console.log(2)}Copy the code
But the latter declaration can still override the previous one
foo() 3 / / output
var foo
function foo() { console.log(1)}function foo() { console.log(3) }
foo = function() { console.log(2)}Copy the code
Let and const
The lets and const provided in ES6 can also be used to define variables, which must be assigned and cannot be changed (memory addresses cannot be changed if they are variables, more on that later). Variables created using lets and const do not generate variable promotion, and create a temporary dead band in their scope that cannot be used until the variable is declared.
{
x = 1 // ReferenceError: Cannot access 'x' before initialization
let x
}
for (let i = 0; i < 10; i++) {}console.log(i) // ReferenceError: i is not defined
Copy the code
Scope closures
Closures are created when a function can remember and access its lexical scope.
function outer() {
var a = 2
function inner(b) {
a += b
return a
}
// Bar remembers the current scope and can access it
return inner
}
const func = outer()
console.log(func(2)) / / 4
console.log(func(2)) / / 6
console.log(func(2)) / / 8
console.log(func(2)) / / 10
Copy the code
In this chestnut,inner can access outer’s inner scope, and then we pass inner as a value to func, which we simply call with a different identifier to inner. In principle, the inner scope should be garbage collected after outer, but the closure prevents this from happening because inner is still referenced by the outer func, so the inner scope can live forever without being collected. Inner always holds a reference to this scope, which is called a closure.
So closures aren’t a particularly esoteric concept, they’re all over our code.
🌰
/ / ajax
let data = []
$.ajax({
url: 'xxx'.data: {},
method: 'post'.success(res) {
data = res.data // Generate closure}})// Event listener
const btn = document.querySelector('#button')
let count = 0
btn.addEventListener('click'.function() {
count++ // Generate closure
}, false)
Copy the code
Need to pay attention to
Since scopes are not garbage collected and consume memory, some extreme cases (at least not yet) can cause a memory leak, so we can manually clear the closure, which is to dereference the function.
function outer() {
var a = 2
function inner(b) {
a += b
return a
}
// Bar remembers the current scope and can access it
return inner
}
const func = outer()
console.log(func(2)) / / 4
func = null // Unreference the internal function, and the garbage collection will be collected
Copy the code
Well, the above is my reading “JavaScript you do not know (1)” reading notes, I hope to help you, if enough time is best to buy a copy of their own look better. There are some modularity applications to closures in the book that I feel I can write an article about, but I won’t go into them here. Thanks for watching