Before we start this article, let’s look at a code snippet like this

// Does the console output 2 or 3?
var a = 2

function foo() {
  console.log(a)
}

function bar() {
  var a = 3
  foo()
}

bar()
Copy the code

If you don’t know the answer, or if you do know the answer but don’t know why, take some time to read this article!

The last article covered some of the concepts of scope and lexical analysis. Scope is a set of rules that govern the scope of the availability of variables in different code. It is also a language-independent concept, and there are two main working models:

  • Lexical scope: The most common, adopted by most programming languages
  • Dynamic scope: Some programming languages are still in use (Bash, some patterns in Perl, etc.)

Lexical scope vs. dynamic scope

Lexical scope

A lexical scope is also called a static scope, and its scope is determined during the lexical analysis phase. In other words, lexical scope is determined by where you scope variables and blocks in your code.

From the figure above we can see that the division of scope blocks in the lexical scoping model is determined by the code structure, and they are included in a hierarchical manner. Foo and bar at the same level have no way to access variables in each other’s block scope. So the output of this JavaScript code is 2 (the variable a is found in foo’s parent scope, the global scope).

Dynamic scope

Dynamic scope is determined dynamically at run time based on the flow information of the program, rather than at the lexical analysis stage.

Let’s re-implement the code snippet at the beginning of this article with a piece of shell code

#! /bin/bash

a=2

foo() {
  echo $a
}

bar() {
  a=3
  foo
}

bar
Copy the code

You guessed right, the output of this code is 3. Dynamic scope does not care about how or where functions and scopes are declared, only where they are called from.

Let me just draw a picture

When the variable a needs to be referenced in the foo method, the variable a is looked up along the call stack. A layer-by-layer lookup of the call stack will find the variable a with the value 3 in the nearest place.

summary

Javascript has only lexical scope and is straightforward. However, its eval(…) The, with, and this mechanisms are somewhat similar to dynamic scoping. We’ll look at eval(…) below. And with.

Major differences: Lexical scope is determined at code writing or definition time, whereas dynamic scope is determined at run time (as is this!). . Lexical scope is concerned with where functions are declared, while dynamic scope is concerned with where functions are called from.

Variable search

The inclusion relationships between scoped blocks give the JavaScript engine enough location information to find the location of identifiers.

For a variable lookup, the engine will start the lookup in the current scope, and if it cannot find it, it will go to the next level of scope.

The scope stops when the first matching variable is found. Variables with the same name can also be defined in multiple nested scopes, but variables with the same name in a higher level scope are not actually accessible. This is called the “shading effect” (internal variables mask external variables).

var a = 1

function foo() {
  var a = 2 // The obscured variable a
  function bar() {
    var a = 3
    console.log(a) / / 3
    console.log(global.a) / / 1
  }
  return bar
}

foo()()
Copy the code

Note: In JavaScript, global variables are automatically properties of the global object, so they can be accessed directly from the global object (global in Node or window in the browser). Masked global variables can be accessed through this technique, but non-globals cannot be accessed in any way if they are masked.

At the same time, the lexical scope looks for only level 1 identifiers. For example, foo.bar.baz, the lexical scope will only attempt to find the identifier foo. Once this variable is found, the object property access rules will take over access to bar and baz.

Cheating on lexical

Lexical scope is defined entirely by where functions are declared during lexical parsing, but there are two mechanisms in JavaScript to “modify” (or cheat) lexical scope at run time.

The community generally felt that using either mechanism was a bad idea. But let’s see how these two mechanisms work.

eval

eval(…) The function takes a string as an argument and treats its contents as code (not a pure string). (It smells like XSS.)

When the code following the eval function is executed, the engine is not aware that the preceding code is inserted dynamically and has made changes to the lexical scope environment. The engine will just do what it always does.

function foo(codeSnippet, a) {
  eval(codeSnippet)
  return a + b
}

var b = 2

foo('var b = 3'.1) // The return value is 4
Copy the code

The code called ‘var b = 3’ in the eval function is treated as if it had been written there.

Note: In this example, the snippet passed to eval is fixed for brevity, but in practice eval(…) can be easily modified according to logic. Into the refs. In strict mode, eval has its own lexical scope at run time, meaning it cannot modify the scope in which it is currently located.

function foo(codeSnippet) {
  "use strict"
  eval(codeSnippet)
  console.log(a) // ReferenceError: a is not defined
}

foo('var a = 2')
Copy the code

with

Another JavaScript feature not currently recommended for fooling lexical scopes is the with keyword.

With is often used as a quick way to repeat references to multiple properties of the same object without having to repeat references to the object itself.

var obj = {
  a: 1.b: 2.c: 3,}// The common mode of modification
obj.a = 2
obj.b = 3
obj.c = 4

/ / to use with
with(obj) {
  a = 3
  b = 4
  c = 5
}
Copy the code

But with isn’t just about easy access to object properties; unexpected things can happen when you use it.

function foo(obj) {
  with(obj) {
    a = 1}}var obj1 = {
  a: 0,}var obj2 = {}

foo(obj1)
console.log(obj1.a) / / 1

foo(obj2)
console.log(obj2.a) // undefined
console.log(a) // 1 -> the variable a leaks into the global scope
Copy the code

When the object has the same properties as the with operation, everything looks fine. However, an attribute is not added to an object when there is no name attribute in the object to modify. Why is that?

With essentially treats the object as a fully isolated lexical scope, so the object’s properties are treated as variables defined in the current scope. In this case, the operation in the with keyword is equivalent to the LHS query mentioned in the previous article. If the variable is found in the current scope, it will be assigned, and if it is not found, it will continue to look up. In this example, a variable is not found in the global scope, so the engine creates a variable in the global scope and assigns it to 1.

Whereas eval takes a snippet of code and modifies its lexical scope, with creates an entirely new scope based on the object passed in.

performance

Although the eval (…). And with can help us achieve more complex functionality and enhance the extensibility of the code. However, some of the performance optimizations that the JavaScript engine does at compile time depend on static analysis while the code is being lexical interpreted.

The engine predetermines where all variables and functions are defined so that variables can be quickly found during execution. If you use eval(…) Or with, the engine can only assume that all previous judgments about the position of the variable are invalid. The most pessimistic case is that if you use both of them, all optimizations may be meaningless.

Therefore, using them has a great impact on the performance of the program, and unreasonable use will cause unexpected results, which should be avoided as far as possible.

summary

Lexical scope means that the scope is determined by where the function is declared when the code is written.

JavaScript has two mechanisms to “trick” lexical scopes: eval(…) And with. The side effect is that the engine cannot optimize for scope lookups at compile time because the engine can only be careful to consider such optimizations invalid. The use of these two mechanisms will inevitably cause your code to run slower. Don’t use them!