Scope is a very basic but very important concept in JS, often appear in the interview, this article will be detailed in-depth explanation of this concept and other related concepts, including declaration promotion, block level scope, scope chain and scope chain extension and other issues.

What is scope

The first problem is figuring out what scope is, which is not just a JS concept, but a universal concept in the programming world. Let’s take this statement as an example:

let x = 1;
Copy the code

This simple statement contains several basic concepts:

  1. Variable: x is a variable, a symbol used to refer to a value.
  2. value(value) : indicates specific data, such as numbers, strings, and objects. here1It’s just a value.
  3. Variable bindings(name binding) : establishes a mapping between variables and values,x = 1Is to take the variablesxand1It’s connected.
  4. Scope: A scope is the valid scope of a name binding. That is, the variable binding is valid in this scope and invalid outside this scope.

As far as programming is concerned, scopes are divided into static scopes and dynamic scopes.

Static scope

Static scope is also called lexical scope, JS is static scope, such as the following code:

let x = 10;

function f() {
  return x;
}

function g() {
  let x = 20;
  return f();
}

console.log(g());  / / 10
Copy the code

In the above code, the x returned by f is the outer definition of x, that is, 10. When we call g, there is a variable x inside g, but we don’t use it here, we use the x inside F. That is, when we call a function, if the variable of the function is not defined in the function, we go to the place where the function is defined. This search relationship is actually determined when we write the code, so it is called static scope. This is a very simple piece of code, everyone knows the output is 10, can you print 20? There is an output of 20, that is dynamic scope!

Dynamic scope

Perl uses dynamic scope, or the same code logic as above. Perl looks like this:

$x = 10;

sub f
{
	return $x;
}

sub g
{
	local $x = 20;
	return f();
}

print g();
Copy the code

The output of the above code is 20, which you can run down in Perl. This is dynamic scope. Dynamic scope is when we call a function, if the variable of the function is not defined in the function, we go to the place where the function is called. Because a function may be called in more than one place, the value of the variable may be different each time it is called, it is called dynamic scope. The variable value of dynamic scope is difficult to determine before running, and the complexity is higher, so the current mainstream is static scope, such as JS,C,C++,Java, these are static scope.

A statement in advance

Variable declaration ahead of time

Before ES6, we used var to declare variables. Variables declared with var were in the scope of functions, that is, visible in the body of functions. One of the problems caused by this was premature declaration.

var x = 1;
function f() {
  console.log(x);
  var x = 2;
}

f();
Copy the code

The output of this code is undefined, because the variable x in function F is declared by var, so it is visible in the whole function f. In other words, it is declared at the top of f, but it is assigned at the time of operation x = 2, so var x = 2; If x is undefined, the above code is equivalent to:

var x = 1;
function f() {
  var x
  console.log(x);
  x = 2;
}

f();
Copy the code

Function declaration advance

Take a look at this code:

function f() {
  x();
  
  function x() {
    console.log(1);
  }
}

f();
Copy the code

The above code x() call will succeed because the declaration of the function will also be preceded by the current function, i.e., the above function x will be preceded by the top of f. The above code is equivalent to:

function f() {
  function x() {
    console.log(1);
  }
  
  x();
}

f();
Copy the code

Note, however, that the above x function will not work if replaced by a function expression:

function f() {
  x();
  
  var x = function() {
    console.log(1);
  }
}

f();
Copy the code

Uncaught TypeError: x is not a function. Because x is a normal variable, but its value is a function, it will be declared at the top of the current function, but as mentioned above, it will be undefined, and undefined as a function call will definitely be TypeError.

Pre-priority of variable and function declarations

Since variable declarations and function declarations are both advanced, which one takes precedence? The answer is that the ** function claims a higher priority! ** Look at the following code:

var x = 1;
function x() {}

console.log(typeof x);  // number
Copy the code

Above we declare a variable x and a function x, both of which have the same name. The x variable is declared as a function, and when we declare x with var, the var is ignored, but the assignment of x=1 is run, and x is 1, of type number.

Block-level scope

Typeerrors are often encountered by beginners who are not familiar with JS, and the code they write may also contain bugs. To solve this problem, ES6 introduces block-level scopes. Block-level scope means that variables are accessible within a specified code block, i.e. within a pair of {}, but not outside. To distinguish var from its predecessor, block-level scopes use let and const declarations, where let declares variables and const declares constants. Look at the following code:

function f() {
  let y = 1;
  
  if(true) {
    var x = 2;
    let y = 2;
  }
  
  console.log(x);   / / 2
  console.log(y);   / / 1
}

f();
Copy the code

The code within the body of the function we use the let stated above a y, by this time his scope is the entire function, and then had an if, the if use the var statement inside a x, using the let again declared a y, because var is function scope, so outside the if you can access to the same x, print it out is 2, The y inside of if is declared by let, so it is block-level scope, that is, it only works inside if. If you print y outside, you get the original y, which is 1.

Duplicate declarations are not allowed

Block-level scopes are not allowed to be declared twice within the same block, for example:

var a = 1;
let a = 2;
Copy the code

Uncaught SyntaxError: Identifier ‘a’ has already been declared

But if you use var declarations, you won’t get an error:

var a = 1;
var a = 2;
Copy the code

Student: No variable promotion?

Variables declared with lets and const are not promoted. In fact, this statement is not accurate, as in the following code:

var x = 1;
if(true) {
  console.log(x);
  
  let x = 2;
}
Copy the code

Uncaught ReferenceError: Cannot access ‘x’ before initialization If x is not promoted, console should get the x defined by the outer var in front of it. It is not accurate to say that the variable does not promote at all because the executor in the if block knows in advance that x is declared by the let below. However, the behavior after promotion is different from var, which reads undefined ** while block-level promotion creates a temporary dead zone (TDZ). ** A temporary dead zone is an error that occurs when a variable is accessed from the block level at the top of the block level in the variable’s official declaration area, as specified in the ES6 specification.

Applications in loop statements

We also encounter the following problem when calling an asynchronous function in a loop, expecting to get the corresponding loop variable each time, but getting the final loop variable instead:

for(var i = 0; i < 3; i++) {
  setTimeout(() = > {
    console.log(i)
  })
}
Copy the code

This is because setTimeout is asynchronous code, which will be executed in the next event loop, while I ++ is synchronous code, which will be completely executed. By the time setTimeout is executed, I ++ will have been executed, and I will already be 3. In the past, we used self-executing functions to solve this problem:

for(var i = 0; i < 3; i++) {
  (function(i) {
    setTimeout(() = > {
      console.log(i)
    })
  })(i)
}
Copy the code

Now that we have let, we can change var to let directly:

for(let i = 0; i < 3; i++) {
  setTimeout(() = > {
    console.log(i)
  })
}
Copy the code

This writing also applies for… In and for… Of cycle:

let obj = {
  x: 1.y: 2.z: 3
}

for(let k in obj){
  setTimeout(() = > {
    console.log(obj[k])
  })
}
Copy the code

Can we use const to declare loop variables? For (const I = 0; i < 3; I ++), const I = 0 is fine, but I ++ is definitely an error, so this loop is going to run once, and then it’s an error. For the for… In and for… Of loops, const declarations are fine.

let obj = {
  x: 1.y: 2.z: 3
}

for(const k in obj){
  setTimeout(() = > {
    console.log(obj[k])
  })
}
Copy the code

Global objects are not affected

Declare a variable with var in the outermost layer (global scope). The variable becomes an attribute of the global object, and if the global object happens to have an attribute of the same name, it is overwritten.

var JSON = 'json';

console.log(window.JSON);   // JSON is overwritten, output 'JSON'
Copy the code

Using let to declare variables does not have this problem:

let JSON = 'json';

console.log(window.JSON);   // JSON is not overwritten, the same object as before
Copy the code

Let and const are improvements to var. If our development environment supports ES6, we should use let and const instead of var.

The scope chain

Scope chain is actually a very simple concept, when we use a variable, first look in the current scope, if not found, go to the outer scope of the search, if not found, continue to look outside, until the global scope, if not found, an error. For example:

let x = 1;

function f() {
  function f1() {
    console.log(x);
  }
  
  f1();
}

f();
Copy the code

This code prints x in f1, so he’s going to look for the variable in F1, and of course he doesn’t find it, and then he’s going to look in F, and again he doesn’t find it, and then he’s going to look in the global scope, and there he is. This lookup chain is the scope chain.

Scope chain lengthening

The previous example actually had three objects on the scope chain:

F1 scope -> F scope -> global scopeCopy the code

In most cases, the length of the scope chain depends on the number of layers it is currently nested, but some statements can temporarily add a variable object to the front of the scope chain, which is removed after code execution. There are two types of statements that can cause scope extension :try… The catch block for catch and the with statement.

try… catch

This is actually a special case that we use all the time:

let x = 1;
try {
  x = x + y;
} catch(e) {
  console.log(e);
}
Copy the code

So in try we’re using an undeclared variable y, so we’re going to get an error, and then we’re going to go to catch, and catch is going to add a variable e to the front of the scope chain, which is the current error object, and we’re going to be able to use that variable to call on the error object, which is essentially extending the scope chain. The variable e is destroyed after the catch block is executed.

with

The with statement can operate on the scope chain. You can manually add an object to the top of the scope chain. When a variable is searched, this object is searched first.

function f(obj, x) {
  with(obj) {
    console.log(x);  / / 1
  }
  
  console.log(x);   / / 2
}

f({x: 1}, 2);
Copy the code

In this code, the “x” output from “with” takes precedence over “obj”, which is equivalent to manually adding “obj” to the first part of the scope chain, so the output x is 1. The outside of with is still the normal chain of scope, so the output x is still 2. It is important to note that the scope chain in the with statement is determined at execution time, and the engine cannot be optimized, so strict mode does not allow the use of with.

conclusion

  1. A scope is simply the valid range of a variable binding.
  2. JS uses static scope, that is, if a function uses a variable that is not in itself, it will look up where it is defined, not where it is called. The dynamic scope is found where you go to call.
  3. varThe variable is declared in advance, and you can access the variable before you assign it, and the value isundefined.
  4. Function declarations are also advanced and have a higher priority thanvarHigh.
  5. usevarThe expression of the function is actually avarVariable that is called before assignmentundefined(), will directly report an error.
  6. letandconstIs a block-level scope and the valid scope is a pair{}.
  7. Cannot be repeated within the same block-level scope, an error will be reported.
  8. Block-level scopes also have “variable promotion”, but the behavior is similarvarIn contrast, “variable promotion” in a block-level scope creates a “temporary dead zone” where an error is reported prior to the declaration.
  9. useletandconstCan be very convenient to solve the problem of incorrect asynchronous call parameters in the loop.
  10. letandconstVariables declared in the global scope do not become properties of the global object.varWill be.
  11. When a variable is accessed, if the current scope is not there, it will be searched up and down, all the way to the global scope, which is called the scope chain.
  12. try... catchthecatchThe block extends the scope chain, adding an error object to the front.
  13. withThe statement can manually add an object to the front of the scope chain, but it is not available in strict mode.
  14. If your development environment supports ES6, you should use itletandconst, do not usevar.

At the end of this article, thank you for your precious time to read this article. If this article gives you a little help or inspiration, please do not spare your thumbs up and GitHub stars. Your support is the motivation of the author’s continuous creation.

Welcome to follow my public numberThe big front end of the attackThe first time to obtain high quality original ~

“Front-end Advanced Knowledge” series:Juejin. Cn/post / 684490…

“Front-end advanced knowledge” series article source code GitHub address:Github.com/dennis-jian…