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/parsingFlow the lexical unit toAbstract Syntax Tree (AST)
  • Code generationThe 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 namedaAnd stores a value inaIn 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

  • engineResponsible for JavaScript compilation and execution from start to finish
  • The compilerResponsible for syntax analysis and code generation
  • scopeResponsible 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.

  • LHSVariables that appear on the left side of an assignment are called LHS, for examplea=2
  • RHSVariables that appear on the right side of an assignment are called RHS, for examplea=bIn 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.

  • withWith 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.

  1. Anonymous functions do not show meaningful function names on the stack trace, so debugging is difficult.
  2. Cannot reference itself. Unless it’s out of datearguments.callee
  3. 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