After reading “JavaScript you don’t know (Volume 1)”, I felt inspired. After a few months, I felt almost forgotten. Today, I am going to make a note while brushing, and record some key ideas in the book for future review.

Today, I will review and summarize the first part: scope and closure.

1. What is scope

In programming languages, the ability to store and access variables brings state to the program

Rules about where variables are stored and how programs find them are called scopes

1.1 Compilation Principles

Any snippet of JavaScript code is compiled before execution (usually just before execution)

  1. Word segmentation/lexical analysis (Tokenizing/Lexing)

Decompose a string of characters into blocks of code that make sense (to the programming language), called lexical units (tokens)

  1. Analysis/Grammar analysis (Parsing)

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).

  1. Code generation

Transform the AST into executable code

1.2 JS compilation principle

  1. Engine – responsible for compiling and executing JS programs
  2. Compiler – responsible for parsing and code generation
  3. Scope – Is responsible for collecting and maintaining a series of queries made up of all variables (identifiers) and enforcing a set of rules that determine access to these identifiers by currently executing code

Take the assignment of a variable as an example

An assignment to a variable performs two actions:

  1. The compilerIn the currentscopeIn theThe statementA variable (if not previously declared)
  2. At runtime,engineWill be inscopeIn theTo find theThis variable is assigned if it can be found

How does the engine find variables

The engine uses LHS and RHS queries to find variables

An LHS query is performed when a variable appears on the left side of an assignment (find the location of the store and assign it) and an RHS query is performed when a variable appears on the right side (find its specific value)

LHS and RHS meaning “left or right side of assignment” does not necessarily mean “= left or right side of assignment operator”.

Assignment operations take several other forms, so conceptually they are best understood as “Who is the target of an assignment operation (LHS)” and “who is the source of the assignment operation (RHS)”

A simpler way to remember is: [left to find the position, right to find the value]

② Practice of LHS and RHS

function foo(a){ // 2. LHS finds the position of a and assigns 2 to a
	var b = a; // 3. RHS find a value 4. LHS find b position, a value 2
	return a + b; // 5. RHS looks for the value of a
}
var c = foo(2) // 1. RHS finds foo 7. LHS finds c and assigns it to foo(2) 4
Copy the code

(3) a few words of BB

LHS and RHS here remind me of getters and setters. LHS is the equivalent of a setter, finding its position, assigning a value to it. RHS is the equivalent of a getter, finding its value

1.3 Scope chain

When scope nesting occurs, when a variable cannot be found in the current scope, the engine continues to look in the outer nested scope until it finds the variable or reaches the outermost scope (that is, the global scope)

1.4 RHS and LHS cannot be found

If the RHS query does not find the desired variable in all nested scopes, the engine throws ReferenceError.

When the engine executes an LHS query, if the target variable cannot be found in the top-level (global scope), the global scope creates a variable with that name and returns it to the engine

Instead of creating and returning a global variable when an LHS query fails in strict mode, the engine throws a ReferenceError exception similar to that when an RHS query fails

1.5 summarize

  1. Scope is a set of rules for determining where and how to look for variables (identifiers)

  2. If the purpose of the lookup is to assign a value to a variable, an LHS query is used;

If the goal is to get the value of a variable, RHS queries are used

  1. Both LHS and RHS queries start in the current execution scope and follow the scope to find the global scope

  2. LHS can not find an error, RHS can not find an error

2. JS scope

2.1 Lexical scope

There are two main working models of scope: lexical and dynamic scope JS uses the == lexical scope ==

Lexical scope is the scope of the definition at the lexical stage.

The lexical scope is determined by where you write the variable and block scopes when you write the code, so the lexical analyzer leaves the scope unchanged when it processes the code.

Look at the scoped bubble below,

The scope created for foo contains three identifiers: A, bar and b. The scope created for bar contains only one identifier: C

The structure of scoped bubbles and their positional relationships give the engine enough positional information to use to find the location of identifiers. The scoped lookup stops when the first matching identifier is found.

No matter where or how a function is called, its lexical scope is determined only by the position in which the function is declared.

Lexical scope means that the scope is determined by the position of the function declaration when the code is written. The lexical analysis phase of compilation basically knows where and how all identifiers are declared, and thus can predict how they will be looked up during execution.

2.2 Function scope

The meaning of a function scope is that all variables belonging to the function can be used and reused throughout the scope of the function (in fact, nested scopes can also be used)

The advantage of function scope is that the internal implementation can be hidden, and the outer scope cannot access anything inside the wrapper function

On function declarations and function expressions

If function is the first word in the declaration, it is a function declaration; otherwise, it is a function expression

Execute the function expression immediately (IIFE)

(function foo(){/ /... }) ()

(function(){/ /... } ())

(function IIFE(global){/ /... })(window)
Copy the code

2.3 Block-level scope

ES6 variables declared with lets and const have block-level scope

Block-level scope in try/catch

The catch clause of try/catch creates a block scope in which declared variables are only valid inside the catch.

2.4 summarize

Any variable declared in a scope will be attached to that scope

3. Variable promotion

Part of the compile phase is finding all the declarations and associating them with the appropriate scope.

All declarations, including variables and functions, are processed first before any code is executed

Functions are promoted before variables

The declaration itself is promoted, but assignment operations, including assignment of function expressions, are not

4. Scope closures

Closures are a natural consequence of writing code based on lexical scope

Closures occur when a function can remember and access its lexical scope, even if the function is executed outside the current lexical scope

In other words,

The lookup of a variable is in the parent scope where the function is defined, not in the execution place

Look at an example [function as return value]

function foo() {
	var a = 2;
	function bar() {
		console.log(a);
	}
	return bar;
}
var baz = foo();
baz(); / / 2
Copy the code

This is really just calling the internal function bar() with a different identifier reference

Bar executes outside of its own lexical scope

Because bar declares its location in Foo, it has a closure that covers foo’s internal scope, allowing that scope to live forever and not be GC; Bar () holds a reference to this scope, which is called a closure

No matter how an inner function is passed outside its lexical scope, it holds a reference to the original definition scope and uses closures wherever the function is executed.

Let’s look at another example.

function wait(message) {
	setTimeout(function timer() {
		console.log(message);
	}, 1000);
}
wait('Hello, closure! ')
Copy the code

Pass the function (timer) to setTimeout(..) .

Because a timer keeps a reference to the variable message, a timer has a closure that covers the wait scope.

So wait (..) Its internal scope does not disappear after 1000 milliseconds.

Inside the engine, the built-in utility function setTimeout(..) Holds a reference to a parameter (callback function). The engine calls this function (timer), and the lexical scope remains intact during this process.

That’s the closure.

Essentially, whenever and wherever you pass functions (accessing their respective lexical scopes) around as first-level value types, you’ll see closures applied to those functions.

Whenever you use callbacks in timers, event listeners, Ajax requests, cross-window communication, Web Workers, or any other asynchronous (or synchronous) task, you’re actually using closures!

practice

Function as [return value]

function create() {
	let a = 100
	return function() { / / define
		console.log(a)
	}
}
let fn = create()
let a = 200
fn() / / 100
Copy the code

② Function as [parameter]

function print(fn) {
	let a = 200
	fn() / / execution
}
let a = 100
function fn() { / / define
	console.log(a)
}
print(fn) / / 100
Copy the code

The variable lookup is in the functiondefineLook up in the parent scope instead of inperformThe place where

Note: The value of this is determined during execution, not during definition

Application of closures in instance development

Extend for loops and closures

for (var i = 1; i <= 5; i++) {
	setTimeout(function timer() {
		console.log(i);
	}, i*1000);
}
Copy the code

The execution outputs five sixes because the timer is executed after the end of the loop

Here’s why: We try to assume that each iteration in the loop “captures” itself a copy of I at run time. But according to how scopes work, the reality is that although the five functions in the loop are defined separately in each iteration, they are all enclosed in a shared global scope, so there is really only one I.

To do this, we need more closure scopes, especially one for each iteration of the loop.

【 improvement 1】 Consider using IIFE to create scope.

for (var i = 1; i <= 5; i++) {
	(function() {
		setTimeout(function timer() {
			console.log(i);
		}, i*1000); }) (); }Copy the code

However, this IIFE is just an empty scope with nothing, so it still doesn’t achieve the desired effect

[Refinement 2] Create a non-empty scope in IIFE

for (var i = 1; i <= 5; i++) {
	(function() {
		var j = i;
		setTimeout(function timer() {
			console.log(j);
		}, j*1000); }) (); }Copy the code

You need to have your own variable in IIFE that stores the value of I in each iteration

[Improvement 3] Pass as a definition parameter in IIFE

for (var i = 1; i <= 5; i++) {
	(function(j) {
		setTimeout(function timer() {
			console.log(j);
		}, j*1000);
	})(i);
}
Copy the code

Using IIFE within an iteration generates a new scope for each iteration, allowing the timer to enclose the new scope within each iteration, with a variable with the correct value for us to access within each iteration

[Improvement 4] Use the block-level scope of let

for (var i = 1; i <= 5; i++) {
	let j = i;
	setTimeout(function timer() {
		console.log(j);
	}, j*1000);
}
Copy the code

This is essentially what converts a block into one that can be closed

[Improvement 5] Final version

for (let i = 1; i <= 5; i++) {
	setTimeout(function timer() {
		console.log(i);
	}, i*1000);
}
Copy the code

The let declaration in the header of the for loop also has a special behavior. This behavior indicates that the variable is declared not just once during the loop, but every iteration. Each subsequent iteration initializes this variable with the value at the end of the previous iteration.

conclusion

Closures occur when a function can remember and access its lexical scope, even if the function is executed outside the current lexical scope

5. What closures do in real development: Hide data

example

Do closures hide data? They only provide apis

function createCache(){
	const data = {} // The data in the closure is hidden from outside access
	return {
		set: function (key, val){
			data[key] = val
		},
		get: function (key) {
			return data[key]
		}
	}
}
const c = createCache()
c.set('a'.100)
console.log(c.get('a'))
Copy the code

The module

This pattern is called a module in JavaScript. The most common way to implement a module pattern is often referred to as module exposure, a variation of which is shown below.

function CoolModule() {
  let something = "cool";
  let another = [1.2.3];

  function doSomething() {
    console.log(something);
  }
  function doAnther() {
    console.log(another.join("!"));
  }

  return {
    doSomething,
    doAnther,
  };
}

var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnther(); / / 1! 2! 3
Copy the code

Neither the inner scope nor the closure can be created without executing an external function

There are two requirements for the module pattern:

  1. There must be an external enclosing function that must be called at least once (each call creates a new module instance).
  2. Enclosing functions must return at least one inner function so that the inner function can form a closure in the private scope and can access or modify the private state.

Module in singleton mode

Convert the module function to IIFE, call the function immediately and assign the return value directly to the singleton’s module instance identifier foo

var foo = (function CoolModule() {
  let something = "cool";
  let another = [1.2.3];

  function doSomething() {
    console.log(something);
  }
  function doAnther() {
    console.log(another.join("!"));
  }

  return{ doSomething, doAnther, }; }) (); foo.doSomething();// cool
foo.doAnther(); / / 1! 2! 3
Copy the code

conclusion

Modules have two main features:

  1. A wrapper function is called to create the inner scope
  2. The return value of the wrapper function must include at least one reference to the inner function, so that a closure covering the entire inner scope of the wrapper function is created

6. Dynamic scope

Lexical scope is a set of rules about how and where the engine finds variables. Its most important feature is that its definition takes place at the writing stage of the code (assuming you don’t use eval() or with).

Dynamic scopes don’t care how or where functions and scopes are declared, just where they are called from in other words, scope chains are based on call stacks, not nested scopes in code, right

JavaScript doesn’t actually have dynamic scope. It only has lexical scope but the this mechanism is somewhat like dynamic scope

The main difference: lexical scope is determined at code or definition time, while dynamic scope is determined at run time. (This too!) Lexical scopes focus on where functions are declared, while dynamic scopes focus on where functions are called from.

Expand this

The arrow function behaves completely differently from normal functions when it comes to the this binding. It abandons all the rules of the normal this binding and instead overrides the original value of this with the current lexical scope.

This will be introduced in the following reading notes blog