JavaScript Advanced Programming (4th edition) Reading Notes

Chapter 4 _ Variables, scopes, and Memory

4.1 Original and Reference Values

ECMAScript variables can contain two different types of data: raw and reference values. Primitive values are simple data, and reference values are objects made up of multiple values.

When assigning a value to a variable, the JavaScript engine must determine whether the value is a primitive or a reference value. A variable that holds the original value is accessed by value because we are manipulating the actual value stored in the variable.

A reference value is an object held in memory. Unlike other languages, JavaScript does not allow direct access to memory locations, so you cannot directly manipulate the memory space in which an object resides. When you manipulate an object, you are actually manipulating a reference to the object rather than the actual object itself. To do this, the variable holding the reference value is accessed by reference.

4.1.1 Dynamic Properties

Raw and reference values are defined in a similar way, creating a variable and assigning a value to it. However, what can be done with the value after the variable holds it is quite different. For reference values, attributes and methods can be added, modified, and removed at any time. Raw values cannot have attributes, although attempts to add attributes to raw values do not generate an error.

let name1 = "Nicholas"; 
let name2 = new String("Matt"); 
name1.age = 27;
name2.age = 26; 
console.log(name1.age);    // undefined 
console.log(name2.age);    / / 26
console.log(typeof name1); // string 
console.log(typeof name2); // object 
Copy the code

Note that primitive types can be initialized using only the primitive literal form. If you use the new keyword, JavaScript creates an instance of type Object, but behaves like the original value.

4.1.2 duplicate values

In addition to being stored differently, the original and reference values differ when copied through variables. When assigning an original value from a variable to another variable, the original value is copied to the location of the new variable.

let num1 = 5;
let num2 = num1; 
// These two variables can be used independently of each other.
Copy the code

When a reference value is assigned from one variable to another, the value stored in the variable is copied to the location of the new variable. The difference here is that the copied value is actually a pointer to an object stored in heap memory. After the operation is complete, both variables actually refer to the same object, so changes on one object are reflected on the other

let obj1 = new Object(a);let obj2 = obj1;
obj1.name = "Nicholas"; 
console.log(obj2.name); // "Nicholas" 
Copy the code

4.1.3 Passing Parameters

When parameters are passed by value, the value is copied to a local variable (that is, a named parameter, or in ECMAScript parlance, a slot in the Arguments object). When a parameter is passed by reference, the value’s location in memory is stored in a local variable, which means that changes to the local variable are reflected outside the function. (This is not possible in ECMAScript.)

/ / the original value
function addTen(num) {  
  num += 10;  
  return num; 
} 
let count = 20; 
let result = addTen(count); 
console.log(count);  // 20, no change
console.log(result); / / 30
/ / reference value
function setName(obj) { 
  obj.name = "Nicholas";  
} 
let person = new Object(a); setName(person);console.log(person.name);  // "Nicholas" 
Copy the code

Obj accesses the object by reference even if it is passed into the function by value. When the name attribute is set inside a function to obj, objects outside the function also reflect the change, because the object obj points to is stored in global scoped heap memory. Many developers mistakenly believe that when an object is modified in a local scope and the change is reflected globally, it means that parameters are passed by reference. To prove that objects are passed by value, let’s look at the following modified example:

function setName(obj) {  
  obj.name = "Nicholas";    
  obj = new Object(a); obj.name ="Greg";
} 
 
let person = new Object(a); setName(person);console.log(person.name);  // "Nicholas" 
Copy the code

The only change in this example is the addition of two lines of code in setName() to redefine obj as a new object with a different name. When person passes setName(), its name property is set to “Nicholas”. The variable obj is then set to a new object and the name property is set to “Greg”. If person is passed by reference, then person should automatically change the pointer to point to an object whose name is “Greg”. However, when we access Person.name again, it has a value of “Nicholas”, indicating that the original reference remains unchanged after the value of the parameter in the function changes. When obj is overridden inside a function, it becomes a pointer to a local object. The local object is destroyed at the end of the function execution.

4.1.4 Determine the type

While e typeof is useful for raw values, it is less useful for reference values. We usually don’t care if a value is an object, but rather what kind of object it is. To solve this problem, ECMAScript provides the instanceof operator with the following syntax:

result = variable instanceof constructorIf the variable is an instance of a given reference type (as determined by its stereotype chain, more on in Chapter 8), theninstanceofOperator returntrue.Copy the code

By definition, all reference values are instances of Object, so detecting any reference values and Object constructors through the instanceof operator returns true. Similarly, if instanceof is used to detect the original value, false will always be returned because the original value is not an object.

4.2 Execution context and scope

The context of variables or functions determines what data they can access and how they behave. Each context has an associated variable object on which all variables and functions defined in that context reside. Although the variable object cannot be accessed through code, it is used for data processing in the background.

The global context is the outer context. In browsers, the global context is what we call the Window object. All global variables and functions defined by var become properties and methods of the Window object. Top-level declarations using lets and const are not defined in the global context, but have the same effect on scope-chain resolution. The context is destroyed after all of its code has been executed, including all variables and functions defined on it (the global context is destroyed before the application exits, such as closing the web page or exiting the browser).

Each function call has its own context. When code execution flows into a function, the context of the function is pushed onto a context stack. After the function completes execution, the context stack pops the function context, returning control to the previous execution context. The execution flow of an ECMAScript program is controlled through this context stack.

When the code in the context executes, it creates a scope chain of variable objects. This chain of scopes determines the order in which the code at each level of context accesses variables and functions. The variable object of the context in which the code is executing is always at the front of the scope chain. If the context is a function, its activation object is used as a variable object. Active objects initially have only one definition variable: arguments. (This variable is not available in the global context.) The next variable object in the scope chain comes from the containing context, and the next object comes from the next containing context. And so on up to the global context; The variable object of the global context is always the variable object next in the scope chain.

Identifier resolution at code execution is done by searching identifier names down the scope chain. The search process always starts at the front of the scope chain and moves down until the identifier is found. (If the identifier is not found, an error is usually reported.)

Here’s an example:

var color = "blue";  
 
function changeColor() { 
  let anotherColor = "red"; 
  function swapColors() {  
  let tempColor = anotherColor; 
  anotherColor = color;   
  color = tempColor; 
  // Here you can access color, anotherColor, and tempColor
  } 
 
  // Color and anotherColor are accessible here, but tempColor is not
  swapColors(); 
} 
 
// Only color can be accessed here
changeColor(); 
Copy the code

The above code involves three contexts: the global context, the local context of changeColor(), and the local context of swapColors(). There is a variable color and a function changeColor() in the global context. The local context of changeColor() has a variable anotherColor and a function swapColors(), but the variable color in the global context is accessible here. The local context of swapColors() has a variable tempColor that can only be accessed in this context. Neither the global context nor the local context of changeColor() is accessible to tempColor. In swapColors(), you can access variables in the other two contexts because they are parent contexts.

The rectangles in the figure represent different contexts.The internal context can access everything in the external context through the scope chain, but the external context cannot access anything in the internal context. The connections between contexts are linear and ordered. Each context can search for variables and functions in the upper-level context, but no context can search in the lower-level context.

4.2.1 Scope chain enhancement

While there are two main execution contexts: global and function contexts (a third exists inside an eval() call), there are other ways to enhance the scope chain. Some statements cause a temporary context to be added to the front of the scope chain, which is removed after code execution. There are two situations in which this occurs, namely when the code executes in either of the following ways:

  • The catch block of a try/catch statement
  • With statement

In both cases, a variable object is added to the front of the scope chain. For the with statement, the specified object is added to the front of the scope chain; For a catch statement, a new variable object is created that contains the declaration of the error object to be thrown.

function buildUrl() {    
  let qs = "? debug=true"; 
  with(location){    
    let url = href + qs; 
  } 
  return url; 
} 
Copy the code

Here, the with statement takes the Location object as the context, so location is added to the front of the scope chain. The buildUrl() function defines a variable qs. When the code in the with statement refers to the variable href, it actually refers to location.href, which is the property of its variable object. When referring to qs, we refer to the variable defined in buildUrl(), which is defined on the variable object in the function context. The variable URL declared with var in the with statement becomes part of the function context and can be returned as the value of the function. But variable urls like the one declared here using let are not defined outside the with block because they are limited to the block-level usage domain (described later).

4.2.2 Variable declaration

1. Use the var function scope declaration

When a variable is declared using var, it is automatically added to the adjacent context. In a function, the proximity context is the local context of the function. In the with statement, the proximity context is also a function context. If a variable is initialized undeclared, it is automatically added to the global context.

function add(num1, num2) {  
  var sum = num1 + num2;  
  return sum; 
} 
let result = add(10.20); / / 30
console.log(sum);         Error: sum is not a valid variable


// If you omit the var keyword in the example above, sum becomes accessible after add() is called
function add(num1, num2) { 
  sum = num1 + num2;  
  return sum;  
} 
 
let result = add(10.20); / / 30
console.log(sum);         / / 30
Copy the code

The VAR declaration is carried to the top of the function or global scope, before all the code in the scope. This phenomenon is called “ascending.” Promotion lets code in the same scope use variables without considering whether they have been declared. In practice, however, promotion can also lead to the legal but strange phenomenon of using variables before they are declared.

function fn1() {   
  var name = 'Jake'; 
} 
 
// This is equivalent to:
function fn2() { 
  var name;   
  name = 'Jake';
} 
Copy the code

You can verify that the variable will be promoted by printing it before the declaration. The promotion of the declaration means that undefined will be printed instead of Reference Error:

console.log(name); // undefined 
var name = 'Jake'; 
 
function() {    
  console.log(name); // undefined   
  var name = 'Jake'; 
} 
Copy the code

2. Use the block-level scope declaration for lets

The new LET keyword in ES6 is similar to var, but its scope is block-level, which is also a new concept in JavaScript. The block-level scope is defined by a close pair of inclusion braces {}. In other words, if blocks, while blocks, function blocks, and even individual blocks are also the scope of let declaration variables.

if (true) {   
  let a;  
} 
console.log(a); // ReferenceError: A is not defined
 
while (true) {   
  let b; 
}
console.log(b); // ReferenceError: B is not defined
 
function foo() {  
  let c;
} 
console.log(c); // ReferenceError: C is not defined
// No wonder
The // var declaration also causes an error
 
// This is not an object literal, but a separate block
// The JavaScript interpreter will recognize it based on its contents
{   
  let d; 
} 
console.log(d); // ReferenceError: d is not defined
Copy the code

Another difference between let and VAR is that you cannot declare it twice in the same scope. Duplicate var declarations are ignored, while duplicate let declarations raise syntaxErrors.

var a;
var a; // No error will be reported
 
{  
  let b; 
  let b; 
} // SyntaxError: identifier B has already been declared
Copy the code

The behavior of the LET is ideal for declaring iteration variables in loops. Iteration variables declared using VAR can leak out of the loop, which should be avoided.

for (var i = 0; i < 10; ++i) {}
console.log(i); / / 10
 
for (let j = 0; j < 10; ++j) {} 
console.log(j); // ReferenceError: j is not defined
Copy the code

Strictly speaking, let is also promoted in JavaScript runtime, but because of “temporal dead zones” you can’t actually use the let variable before declaration. Therefore, from the standpoint of writing JavaScript code, let is not promoted in the same way as VAR.

3. Use a const constant declaration

In addition to let, ES6 also adds the const keyword. Variables declared using const must also be initialized to a value. Once declared, new values cannot be reassigned at any point in its life cycle.

const a; // SyntaxError: Constant declaration is not initialized
 
const b = 3; 
console.log(b); / / 3
b = 4; // TypeError: Assigns values to constants
Copy the code

Const is the same as a LET declaration except that it follows the rules above. Const declarations apply only to top-level primitives or objects. In other words, a const variable assigned to an object cannot be reassigned to another reference, but the key of the object is not restricted.

const o1 = {}; 
o1 = {}; // TypeError: Assigns values to constants
 
const o2 = {};
o2.name = 'Jake';
console.log(o2.name); // 'Jake' 
Copy the code

If you want the entire Object to be immutable, you can use Object.freeze(), which will silently fail when assigning attributes again, although no errors will be reported.

const o3 = Object.freeze({});
o3.name = 'Jake';
console.log(o3.name); // undefined 
Copy the code

Because a const declaration implies that the value of a variable is of a single type and cannot be modified, the JavaScript runtime compiler can replace all instances of it with the actual value without looking up the variable through a query table.

4. Search for identifiers

When an identifier is referenced for reading or writing in a particular context, a search must be performed to determine what the identifier represents. The search starts at the front of the scope chain, searching for the corresponding identifier by the given name. If the identifier is found in the local context, the search stops and the variable is determined; If the variable name is not found, the search continues along the scope chain. (Note that objects in the scope chain also have a prototype chain, so a search might involve the prototype chain for each object.) This process continues until the variable object is searched to the global context. If the identifier is still not found, it is undeclared.

4.3 Garbage Collection

JavaScript is a language that uses garbage collection, which means the execution environment is responsible for managing memory while the code executes. JavaScript takes this burden off the developer and implements memory allocation and idle resource recycling through automatic memory management. The basic idea is simple: determine which variable will no longer be used, and then free up its memory. This process is periodic, meaning that the garbage collection program runs automatically at certain intervals (or at a predetermined collection time during code execution). The garbage collection process is an approximate and imperfect solution because the question of whether a block of memory is still usable is an “undecidable” problem, meaning that algorithms cannot solve it.

Let’s take the normal life cycle of a local variable in a function. Local variables in a function exist at the time the function is executed. At this point, stack (or heap) memory is allocated space to hold the corresponding value. The function uses variables internally and exits. At this point, the local variable is no longer needed and its memory can be freed for later use. Local variables are obviously no longer needed in this case, but not always. The garbage collector must keep track of which variables will be used again and which variables will not be used again in order to reclaim memory. There may be different implementations of how to mark unused variables. However, in the history of browsers, two major markup strategies have been used: tag cleanup and reference counting.

4.3.1 Mark cleaning

A common JavaScript garbage collection strategy is mark-and-sweep. When a variable is entered into a context, such as declaring a variable inside a function, the variable is marked as existing in the context. Variables in context, logically, should never be freed, because they can be used as long as the code in context is running. Variables are also marked out of context when they are out of context.

There are many ways to tag variables. For example, when a variable comes into context, it reverses a bit; Or you can maintain lists of “in context” and “out of context” variables, and you can move variables from one list to the other. It’s not the implementation of the marking process that matters, it’s the strategy.

When the garbage collector runs, it marks all variables stored in memory (remember, there are many ways to mark them). It then strips out all variables in the context, and all variables referenced by variables in the context. Variables tagged after this point are deleted because they are not accessible to any variables in context. The garbage collector then does a memory cleanup, destroying all tagged values and reclaiming their memory.

4.3.2 Reference Counting

The idea is to keep track of how many times each value is referenced. When you declare a variable and assign it a reference value, the number of references to that value is 1. If the same value is assigned to another variable, the number of references is increased by one. Similarly, if the variable holding a reference to that value is overwritten by another value, the number of references is reduced by one. When the number of references to a value is zero, the value can no longer be accessed, so its memory can be safely reclaimed. The next time the garbage collector runs, it frees memory for the zero reference value.

Serious problem: circular references. A circular reference is when object A has A pointer to object B, and object B refers to object A. That means they all have 2 references. Under the tag cleanup strategy, this is not a problem because neither object is in scope after the function ends. Under the reference-counting strategy, they will survive the end of the function because their number of references will never go to zero.

Workaround: Setting a variable to NULL actually severs the relationship between the variable and its previous reference value. When the next garbage collector runs, these values are deleted and the memory is reclaimed.

4.3.3 performance

Garbage collection programs run periodically, and if many variables are allocated in memory, performance can suffer, so scheduling garbage collection is important. Especially on mobile devices with limited memory, garbage collection can significantly slow down rendering speeds and frame rates. Developers don’t know when garbage will be collected at run time, so it’s a good idea to do this when writing code: whenever garbage collection starts, it will end as soon as possible.

After IE7 was released, the JavaScript engine’s garbage collection program was tuned to dynamically change the threshold for garbage collection by allocating variables, literals, or array slots. The start threshold of Internet Explorer 7 is the same as that of Internet Explorer 6. If the garbage collector reclaims less than 15% of the allocated memory, the thresholds for these variables, literals, or array slots are doubled. If 85% of the allocated memory is reclaimed at one time, the threshold is reset to the default value. This simple change dramatically improves the performance of heavily javascript-dependent web pages in browsers.

4.3.4 Memory Management

In programming environments that use garbage collection, memory management is usually not a concern for developers. However, JavaScript runs in an environment where memory management and garbage collection are special. The amount of memory allocated to browsers is typically much less than that allocated to desktop software, and even less for mobile browsers. This is more for security reasons than anything else, just to keep the operating system from crashing due to javascript-heavy web pages running out of memory. This memory limit affects not only variable allocation, but also the call stack and the number of statements that can be executed simultaneously in a thread.

Keeping the memory footprint to a small value can lead to better page performance. A good way to optimize memory footprint is to ensure that only the necessary data is saved when executing code. If the data is no longer necessary, set it to NULL, freeing its reference. This can also be called dereferencing. This advice applies to global variables and properties of global objects. Local variables are automatically dereferenced when they go out of scope.

1. Improve performance with const and let declarations

The addition of these two keywords in ES6 not only helps improve the code style, but also the garbage collection process. Because both const and let are scoped by blocks (not functions), using these two new keywords may allow the garbage collector to step in and reclaim memory that should be reclaimed sooner than var would. This can happen in cases where the block scope terminates earlier than the function scope.

2. Hide classes and delete operations

Depending on the environment in which JavaScript is running, you sometimes need to take different performance tuning strategies depending on the JavaScript engine your browser is using. As of 2017, Chrome is the popular browser that uses the V8 JavaScript engine. V8 makes use of “hidden classes” when it compiles interpreted JavaScript code into actual machine code. This may be important to you if your code is very performance-oriented. At run time, V8 associates the created objects with hidden classes to track their attribute characteristics. Objects that can share the same hidden class perform better, and V8 is optimized for this, but not always. The solution is to avoid JavaScript’s ready-fire-aim dynamic attribute assignment and declare all attributes at once in constructors,

3. Memory leaks

Poorly written JavaScript can have memory leaks that are difficult to detect and harmful. On devices with limited memory, or in cases where functions are called many times, memory leaks can be a big problem. Memory leaks in JavaScript are mostly caused by improper references.

  • Accidentally declaring global variables is a common but easily fixed memory leak.

  • Timers can also quietly cause memory leaks.

  • Using JavaScript closures can easily cause memory leaks without even knowing it.

4. Static allocation and object pool

In order to improve JavaScript performance, the last thing to consider is often squeezing the browser. At this point, a key issue is how to reduce the number of times the browser performs garbage collection. Developers have no direct control over when garbage collection starts, but they can indirectly control the conditions that trigger garbage collection. In theory, if you can use allocated memory wisely and avoid unwanted garbage collection, you can preserve the performance lost by freeing memory.

One of the criteria the browser uses to decide when to run the garbage collector is the speed of object turnover. If a lot of objects are initialized and then all of a sudden go out of scope, the browser will schedule the garbage collector to run in a more aggressive manner, which of course affects performance. The solution is not to create vector objects dynamically. For example, you can modify the above function to use an existing vector object.

Of course, this requires that the vector argument resultant be instantiated elsewhere, but the behavior of this function remains the same. So where can a vector be created without the garbage collection scheduler noticing it? One strategy is to use object pools. At some point during initialization, a pool of objects can be created to manage a collection of recyclable objects. An application can request an object from the object pool, set its properties, use it, and then return it to the object pool when the operation is complete. Since no object initialization occurs, garbage collection probes do not detect object turnover, so the garbage collection program will not run as often. Here is a pseudo-implementation of an object pool:

// vectorPool is an existing object pool
let v1 = vectorPool.allocate();
let v2 = vectorPool.allocate(); 
let v3 = vectorPool.allocate(); 
 
v1.x = 10;
v1.y = 5; 
v2.x = -3; 
v2.y = -6; 
 
addVector(v1, v2, v3); 
 
console.log([v3.x, v3.y]); // [7, -1] 
 
vectorPool.free(v1); 
vectorPool.free(v2); 
vectorPool.free(v3); 
 
// If the object has attributes that refer to other objects
// Set these properties to NULL
v1 = null;
v2 = null; 
v3 = null; 
Copy the code

If the object pool only allocates vectors on demand (creating new objects when they don’t exist and reusing them when they do), then the implementation is essentially a greedy algorithm with monotonously growing but static memory. The object pool must maintain all objects in some structure, and arrays are a good choice. However, when implementing with arrays, you must be careful not to incur additional garbage collection.

let vectorList = new Array(100);
let vector = new Vector(); 
vectorList.push(vector); 
Copy the code

Since JavaScript arrays are dynamically variable in size, the engine removes the 100 array and creates a new 200 array. The garbage collector will see the deletion and probably pick up the garbage soon enough. To avoid this dynamic allocation, you can create an array of sufficient size at initialization to avoid the delete and then create operations described above. However, you must figure out how big the array is beforehand.

4.4 summary

JavaScript variables can hold two types of values: original and reference values. The original value may be one of six primitive data types: Undefined, Null, Boolean, Number, String, and Symbol. Raw and reference values have the following characteristics.

  • The original value is fixed in size and therefore stored in stack memory.
  • Copying the original value from one variable to another creates a second copy of the value.
  • Reference values are objects stored in heap memory.
  • Variables that contain reference values actually contain only a pointer to the corresponding object, not the object itself.
  • Copying a reference value from one variable to another only copies Pointers, so the result is that both variables point to the same object.
  • The typeof operator determines the original typeof a value, while the instanceof operator is used to ensure the reference typeof a value.

Any variable (whether it contains a raw value or a reference value) exists in some execution context (also known as scope). This context (scope) determines the lifetime of variables and what parts of the code they can access. The execution context can be summarized as follows.

  • Execution context is divided into global context, function context and block-level context.
  • Each time the code execution flow enters a new context, a chain of scopes is created to search for variables and functions.
  • The local context of a function or block can access variables not only in its own scope, but also in any containing context or even in the global context.
  • The global context can only access variables and functions in the global context, and cannot directly access any data in the local context.
  • The execution context of a variable is used to determine when memory is freed.

JavaScript is a programming language that uses garbage collection, so developers don’t have to worry about memory allocation and collection. The JavaScript garbage collector can be summarized as follows.

  • Values that leave scope are automatically marked as recyclable and then deleted during garbage collection.
  • The dominant garbage collection algorithm is tag cleaning, which marks values that are not currently in use and then comes back to reclaim their memory.
  • Reference counting is another garbage collection strategy that keeps track of how many times a value is referenced. JavaScript engines no longer use this algorithm, but some older versions of IE still suffer from it because JavaScript accesses non-native JavaScript objects (such as DOM elements).
  • Reference counting has problems with circular references in your code.
  • Dereferencing variables not only eliminates circular references, but also helps with garbage collection. To facilitate memory reclamation, global objects, properties of global objects, and circular references should all be dereferenced when no longer needed.

References:

JavaScript Advanced Programming (Version 4)