An overview of the
Scope and closure have always been the focus of interviews in various factories. After studying JS for some time, it is time to explain this part of knowledge.
This article does not cover the block-level scope of ES6 for now. This article is an extensive review of JavaScript you Don’t Know.
scope
A scope is a set of rules for storing variables and determining where and how to look for variables (identifiers).
Scope is also commonly understood as the scope in which a variable exists, the current execution context, and so on. In the ES5 specification, JavaScript has only two scopes:
- Global scope: Variables persist throughout the program and can be read anywhere
- Function scope: Variables exist only inside functions
When a function is nested within another function, its scope is also nested, creating a chain of scopes. If a variable or other expression is not in the current scope, the JS mechanism continues to look up the scope chain layer by layer until it reaches the global scope (global or window in the browser). If it cannot be found, it will not be used.
Before the source code is executed, it goes through the compilation process.
The build process
Unlike traditional compiled languages, JavaScript is not compiled ahead of time, and most of the time compilation takes place a few microseconds or less before the code executes. So JavaScript engines use various methods (such as JIT, which delays or even recompiles) to ensure optimal performance.
The whole compilation process is divided into the following steps:
(1) Lexical analysis
This process breaks down a string of characters into meaningful blocks of code called lexical units (tokens).
For example, var a = 2, the program breaks down into these lexical units: var, a, =, 2. Whether the Spaces between are considered lexical units depends on whether they make sense.
(2) Grammatical analysis
This process converts a stream of lexical units (arrays) into a hierarchical nested tree of elements that represents the syntactic structure of the program. This Tree is called the Abstract Syntax Tree (AST).
This tree defines the structure of the code. By manipulating this tree, we can accurately locate the statement, assignment statement, operation statement and so on, and realize the analysis, optimization, change and other operations of the code.
Here’s an example:
var global1 = 1
Copy the code
The AST of this code is as follows (Parser: Acorn-6.1.1) :
{
"type": "VariableDeclaration"."declarations": [{"type": "VariableDeclarator"."id": {
"type": "Identifier"."name": "global1"
},
"init": {
"type": "Literal"."value": 1."raw": "1"}}]."kind": "var"
}
Copy the code
For a more complex example (see full syntax tree) :
var global1 = 1
function fn1(param1){
var local1 = 'local1'
var local2 = 'local2'
function fn2(param2){
var local2 = 'inner local2'
console.log(local1)
console.log(local2)
}
function fn3(){
var local2 = 'fn3 local2'
fn2(local2)
}
fn3() // 'local1'
// 'inner local2'
}
fn1()
Copy the code
If only variable declarations are analyzed, the AST can be simplified as follows:
The whole analysis process is completed in the static stage, so fn2 in FN3 has already determined its declaration position in the syntax analysis stage, and when fN1 is called, it is clear that the scope of Fn2 is in the function structure of FN1, and the function scope of FN3 does not affect it. So the printed local2 value is ‘inner local2’ instead of ‘fn3 local2’.
There are several common uses of AST:
- Code syntax checks, code style checks, code formatting, code highlighting, code error prompts, code auto-completion, and so on
- JSLint, JSHint check for code errors or styles, find some potential errors IDE error prompts, formatting, highlighting, autocomplete, etc
- Code obfuscation compression
- UglifyJS2 etc.
- Optimization changes the code, changes the code structure to achieve the desired structure
- Code packaging tools webpack, Rollup, CommonJS, AMD, CMD, UMD and other code specifications between CoffeeScript, TypeScript, JSX and other conversion into native Javascript
(3) Code generation
The process of converting an AST into executable code.
Code generation is the transformation of the AST from the previous step into machine instructions, and then storing them in memory.
Lexical scope
Is the scope of the definition at the lexical stage. The scope of a variable is determined at definition time, not at execution time. That is, the lexical scope depends on the source code and can be determined by static analysis. Therefore, lexical scope is also called static scope (with and eval can trick lexical scope).
Var a = 2
- Compiler encounters
var a
The scope is asked if there is a variable with that name. If so, ignore and continue compiling; If not, declare the variable in the current scope, named asa
. - Engine execution code
a = 2
, can querya
(LHS query) and assign to it.
There are two types of queries:
- LHS (Left Hand Side) : The search purpose is to assign a value to a variable
- RHS (Right Hand Side) : The search purpose is to obtain the value of a variable
Both LHS and RHS queries start at the current execution scope, and if the desired identifier is not found, the query continues to the upper scope, up to the global scope. At global scope, ReferenceError is raised if the RHS query fails, and a global variable is implicitly created if the LHS query fails (non-strict mode).
Here’s an example:
function foo(a) {
var b = a
return a + b
}
var c = foo(2)
Copy the code
- The engine performs
var c = foo(2)
, will look in the scope to see if (RHS) hasfoo
function - When found, the argument is taken
2
Assign a value to the parametera
(LHS, Implicit variable assignment) var b = a
Find the variables firsta
(RHS)- will
a
Assigns the value tob
(LHS) return a + b
, search separatelya
和b
Value of (twice RHS) and then return- will
foo(2)
Assigns the result of thec
(LHS)
Global scope
Take the browser environment as an example:
- Outermost functions and variables defined outside the outermost function have global scope
- All variables that do not have a direct assignment defined are automatically declared to have global scope
- All properties of window objects have global scope
Disadvantages: contaminates the global namespace.
Solution:
- The Immediately Invoked Function Expression (IIFE) is Invoked, so the source code for many libraries is used
- Modularity (ES6, CommonJS, etc.)
Function scope
A function scope means that all variables belonging to the function can be used and reused throughout the function scope.
function foo() {
let name = 'Shawn'
function sayName() {
console.log(`Hello, ${name}`)
}
sayName()
}
foo() // 'Hello, Shawn'
console.log(name) // The external cannot access the internal variable
sayName() // The external cannot access the internal function
Copy the code
closure
Closures occur when a function can remember and access its lexical scope, even if the function is executed outside the current lexical scope.
For example, the simplest closure (function + a variable that can be accessed inside a function) :
var local = "Variable"
function foo () {
console.log(local)
}
Copy the code
But then local is exposed to the global scope and other functions can access it. Also, this is only meant to be accessible, not “remembered”. So you need to add some code to the closure so that the variable local is local to Foo, and foo can be accessed externally. Some implementations:
(1) Encapsulate with immediate execution functions, and add the required functions as global variables of window
!function(){
var local = "Variable"
window.foo = function (){
console.log(local)
}
}()
foo()
Copy the code
(2) Anonymous function expression, return the required function as an argument
var a = function(){
var local = "Variable"
function foo(){
console.log(local)
}
return foo
}
var myFoo = a()
myFoo() // This is what closure does
Copy the code
In the example above (2), foo was designed to access the local variable because it needed to access the parameters and variables of the external function. Foo could access the local variable and return foo as an argument in the external function, according to the nested function “internal function can access the parameters and variables of the external function”. Thus, when an anonymous function is assigned to a and then the result of a’s execution is assigned to myFoo, it is equivalent to myFoo = foo, and executing myFoo achieves the purpose of remembering and accessing foo’s lexical scope.
Also, after a() is executed, its internal scope is not destroyed by GC because of the closure, because foo() continues to use the internal scope.
No matter how an inner function is passed outside of its lexical scope, it retains a reference to the original definition scope, and closures are used wherever the function is executed.
Loops and closures
Here’s a classic example:
for (var i = 1; i <=5; i++) {
setTimeout(function timer(){
console.log(i)
}, i * 1000)}Copy the code
When I first looked at this code, I expected it to print the numbers 1 to 5, one per second, one at a time. The actual result, however, is to print 6’s five times, once per second.
According to?
First, the value of I is 6 at the end of the loop, and then the callback to the delay function is executed at the end of the loop. Even if setTimeout(… 0), the result is still the same.
Why?
SetTimeout is executed asynchronously. After 1000 milliseconds, a task is added to the task queue. The task in the task queue will be executed only when all the tasks on the main thread are executed. So I is all 6. Because the I declared by var in the for loop is in the global scope, there is only one I in the whole loop. Although the five functions in the loop are defined separately in their respective iterations, they share the reference to the one I, so the printed I in the timer function is naturally 6.
So, we need to set a closure scope for each iteration in the loop.
Try executing functions now (IIFE) to solve this problem.
First attempt:
for (var i = 1; i <=5; i++) {
!function () {
setTimeout(function timer(){
console.log(i)
}, i * 1000()})}Copy the code
This doesn’t work, however, because the scope of the anonymous function is empty and it has no real content to use. There’s still only one I.
Second attempt:
for (var i = 1; i <=5; i++) {
!function () {
var j = i
setTimeout(function timer(){
console.log(j)
}, j * 1000()})}Copy the code
It worked! But the code doesn’t look very elegant.
Third attempt:
for (var i = 1; i <=5; i++) {
!function (j) {
setTimeout(function timer(){
console.log(j)
}, j * 1000)
}(i)
}
Copy the code
Thus, using IIFE within an iteration generates a new scope for each iteration, allowing the callback of the delay function to enclose the new scope within each iteration, and each iteration contains a variable with the correct value for us to access.
So, how is this problem solved after ES6? Let came to mind first, which can be used to hijack the block scope and declare a variable within the block scope.
Fourth attempt:
for (var i = 1; i <=5; i++) {
let j = i // Block scope for closure
setTimeout(function timer(){
console.log(j)
}, j * 1000)}Copy the code
So is this the ultimate answer? First look at the code:
Fifth attempt:
for (let i = 1; i <=5; i++) {
setTimeout(function timer(){
console.log(i)
}, i * 1000)}Copy the code
And this last way, this is the way it’s written today. It’s a syntactic sugar, and inside it is fourth-try writing code. Here the scope of I is only for(…) With each iteration, JS will automatically redeclare an I in {… }, the I of each subsequent iteration is initialized with the value at the end of the previous iteration.
The purpose of closures
(1) Store and hide variables
One use of closures is to read variables inside a function and keep those variables in memory at all times, that is, a closure can keep the environment in which it was born. And because they are internal variables of the function, local variables can not be accessed externally, and also achieve the purpose of hiding.
function createCounter(initial) {
var x = initial || 0
return {
inc: function () {
x += 1
return x
}
}
}
var c1 = createCounter()
c1.inc() / / 1
c1.inc() / / 2
c1.inc() / / 3
var c2 = createCounter(1024)
c2.inc() / / 1025
c2.inc() / / 1026
c2.inc() / / 1027
Copy the code
In the above example, x is the internal variable of the function createCounter. With closures, the state of X is preserved, and each call is evaluated on the basis of the previous call. The inc presence depends on createCounter and therefore is always in memory and is not collected by the garbage collection mechanism after the call ends. This allows the variable X to be stored and hidden.
So, a closure can be thought of as an interface to a function’s internal scope.
(2) Encapsulate private variables
Since JavaScript attributes don’t have modifiers like public and private to control access, and all attributes need to be defined in functions, we need some means to privatize variables.
var Foo = function () {
var _name = 'Frank'
this.getName = function () {
return _name
}
this.setName = function (str) {
_name = str
}
}
var foo1 = new Foo()
foo1.setName('Shawn')
var foo2 = new Foo()
foo2.setName('Givenchy')
foo1._name // undefined, external cannot directly access local variables, equivalent to "private"
foo1.getName() // 'Shawn'
foo2.getName() // 'Givenchy'
Copy the code
In the above example, the internal variable _name of function Foo, through the closures setName and getName, becomes the private variable of the return objects foo1 and foo2, and they are independent of each other.
More generally, you can see closures applied to functions that access their respective lexical scopes essentially whenever and wherever you pass them around as first-level value types. Whenever you use callbacks in timers, event listeners, AJAX requests, cross-window communication, Web Worders, or any other asynchronous (or synchronous) task, you’re actually using closures.
performance
It is unwise to create functions in other functions that do not require closures for some specific task, because closures have a negative impact on script performance in terms of processing speed and memory consumption.
For example, when creating a new object or class, methods should usually be associated with the object’s prototype rather than defined in the object’s constructor. The reason is that this causes the method to be reassigned each time the constructor is called (that is, each object created).
For example, the closure of the above example that encapsulates private variables is better defined on a prototype:
var Foo = function () {
var _name = 'Frank'
Foo.prototype.getName = function () {
return _name
}
Foo.prototype.setName = function (str) {
_name = str
}
}
Copy the code
Inherited stereotypes can be shared by all objects without having to define methods every time an object is created.
conclusion
-
Q: What is scope? A: Scope is A set of rules for determining where and how to look for variables.
-
Q: What is a scope chain? A: Scope nesting occurs when A function is nested within another function. If a variable is not found in the current scope, the JS engine will continue to look in the outer nested scope until it finds the variable or reaches the global scope. An error is reported if it is not found in global scope. This pattern of ascending searches is the scope chain.
-
Q: What is a closure? A: Closures occur when A function can remember and access its lexical scope, even if the function is executed outside the current lexical scope.
After brushing some questions, I will pick out some classic questions and summarize them for interview and deepening.
Reference:
- An introduction to abstract syntax trees
- Abstract syntax tree
- MDN: Closures
- Efficient use of JavaScript closures
- Private variables in JavaScript
- Closures and private variables