portal

This series of articles is for front-end developers who want to systematically learn, advance JavaScript, or literacy

  • JavaScript knowledge you must Understand – Basics

  • JavaScript you must understand — variables, scopes, memory

  • JavaScript you must understand — reference types

Variables, scope, and memory

  • Use raw and reference values through variables
  • Understand the execution Context
  • Understand garbage collection

In contrast to other languages, JavaScript variables are loosely typed, and their values and data types can be changed at will throughout the life of the script. This is powerful, but it also creates problems.

Original and reference values

ECMAScript contains two types of data: raw values and reference values. Raw values are the simplest data, and reference values are objects made up of multiple values.

Primitive values include Undefined, Null, Boolean, Number, String, and Synbol. Raw values are accessed by value and operate on the actual value stored in the variable.

A reference value is an object held in memory. JavaScript does not allow direct access to memory, so when you manipulate an object, you manipulate a reference to that object rather than the actual object. Therefore, variables that reference values are accessed by reference.

Dynamic properties

Dynamic attributes are instructions for dynamically adding attributes to a type variable after it has been created. Raw values cannot have attributes, although adding attributes to raw values is not an error.

/ / object
const obj = new Object(a);// Add attributes dynamically
obj.name = 'lyn'
console.log(obj.name)	// lyn
/ / the original value
const vari = 'test'
vari.name = 'lyn'
console.log(vari.name)	// undefined
Copy the code

Duplicate values

In addition to the way original and reference values are stored, they are also different when variables are copied. When copying, the variable itself is copied, so the original value is copied completely, and the two variables after copying do not interfere with each other. But the reference value variable stores a memory address, so the actual copy of a pointer, the old and new variables will specify the same object stored in heap memory.

The original value

// The two variables can be used independently of each other
let num1 = 5;
let num2 = num1;
console.log(num1, num2)	/ / 5 5
num1 = 6;
num2 = 8
console.log(num1, num2)	/ / 6, 8
Copy the code

Reference value

// The last two variables operate on the same object
const obj1 = { t: 't' }
const obj2 = obj1
console.log(obj1, obj2)	// {t: "t"} {t: "t"}
obj1.t = 'tt'
console.log(obj1, obj2)	// {t: "tt"} {t: "tt"}
Copy the code

Passing parameters

ECMAScript variables are accessed by value and by reference, but function parameters are passed by value, including the value of the reference type.

When arguments are passed by value, the value is copied to a local variable of the function (named arguments, or in ECMAScript terms, a slot in the Arguments object). To illustrate:

const gNum = 10
function fn(num) {
  num += 10;
  console.log(num)	// 20, local variable num value changed
  console.log(gNum) // 10, the outer num is unchanged
}
fn(num)
Copy the code

This example shows that raw values are passed by value

const gObj = { t: 't' }
function fn(obj) {
  console.log(obj) // { t: 't' }
  obj.t = 'tt'
  // The value of the gObj accessing the outer layer is changed only to indicate that obj and gObj are holding the same pointer
  console.log(gObj) // { t: 'tt' }
  // Override the value of the obj variable
  obj = { c: 'c' }
  // Obj stores a pointer to a value variable
  console.log(obj) // { c: 'c' }
  console.log(gObj) // { t: 'tt' }
}

fn(obj)
Copy the code

This example is sufficient to show that variables of reference type are also passed by value when passed by function, because if passed by reference, gObj should be overridden when obj is overridden.

The important thing is to understand what is stored inside the original value variable and reference type variable.

Determine the type

Determine the type of a variable in the ECMAScript 5 kinds of methods: typeof operator, the instanceof operator, Array. The isArray () method, constructor attribute, Object. The prototype, the toString () method.

Typeof is great for determining string, numeric, Boolean, undefined and function types, but not for objects, null, arrays, and so on.

Instanceof and constructor are commonly used to determine whether a specified variable is an instanceof an object.

The array. isArray(variable) method is used to check whether the specified variable is an Array, which can solve the problem of the quadrate frame

String. The prototype. ToString. Call () method, perfect judgment method of variable types.

// typeof
console.log(typeof 'test')	// string
console.log(typeof 2)	// number
console.log(typeof true)	// boolean
console.log(typeof undefined)	// undefined
// Typeof does not work well for the following types
console.log(typeof null)	// object
console.log(typeof {})	// object
console.log(typeof [])	// object
console.log(typeof new Date())	// object
console.log(typeof function () {})	// function
console.log(typeof Array)	// function

/ / instanceof and constructor
const d = new Date(a)console.log(d instanceof Date)	// true
console.log(d instanceof Array) // false
console.log(d.constructor === Date)	// true
console.log(d.constructor === Array) // false

/ / Object. The prototype. ToString. Apply (variable), return to the [Object] Xxx
console.log(Object.prototype.toString.apply(2))	// [object Number]
console.log(Object.prototype.toString.apply(null))	// [object Null]
console.log(Object.prototype.toString.apply(Array))	// [object Function]
console.log(Object.prototype.toString.apply({})) // [object Object]
console.log(Object.prototype.toString.apply(d)) // [object Date]
Copy the code

Ecma-262 specifies that any object that implements an internal [[Call]] method should return function when Typeof is detected

Execution context and scope

The execution context (” context “) determines what data variables and functions can access and how they behave. Each context has an associated variable object on which all variables and functions defined in the context reside. Although the variable object cannot be accessed through code, it is used in background processing of data. There are three types of context: global context, function context, and context inside an EVAL call.

The object of the global context may be different depending on the host environment that implements ECMAScript. For example, in the browser, the global context is the Window object. All global variables and functions declared 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 work just as well on scope-chain resolution. The context is destroyed after the execution of its left and right code, and the global context is destroyed before the application exits, such as closing the web page and browser.

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

The eval function can set the global context when it is called, thus forming a separate context. For example, the JS sandbox of the Qiankun framework uses the eval function.

Context code, when executed, creates a scope chain for the variable object. The variable object of the global context is always at the end of the scope chain, and the variable object of the currently executing context is at the very beginning of the scope chain. Code executes identifier resolution is to pass along the scope chain from the front to start step by step backward search to complete (the scope chain object has a prototype object, so the search may involve each object’s prototype chain), if the search to the global context variable object, haven’t found identifier, and explain the statement.

Finding identifiers on the scope chain has some performance overhead. Accessing local variables is faster than accessing global variables because you don’t have to switch scopes. However, JavaScript engines do a lot of optimization on identifier look-up, and the difference may be trivial in the future.

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

Each rectangle represents a context, and the internal context can access everything about the external context through the scope chain, but the external context cannot access anything about the internal context.

Scope chain enhancement (extension)

The following two statements execute by adding a temporary context (extending the scope chain) to the front of the scope chain, which is removed at the end of code execution.

  • The catch block of a try/catch statement

    The catch statement creates a new variable object (error) containing the error object declaration to be thrown

  • With statement

    The with statement adds a specified object to the very front of the scope chain

function fn() {
  const qs = '? dev=true'
  
  with(location) {
    // var cannot declare block-level scoped variables, so the URL becomes part of the context of the fn function
    var fnUrl = href + qs + '&vartest=1'
    // Const doesn't have var problems because const and let can declare block-level scoped variables
    const urlTest = href + qs
    console.log(urlTest)
  }
  // It can be accessed normally
  console.log(fnUrl)
  // ReferenceError: urlTest is not defined
  // console.log(urlTest)
}
fn()
Copy the code

The garbage collection

In languages like C and C++, memory management needs to be done by the developers themselves. JavaScript is a language that uses garbage collection to allocate memory and recycle idle resources through automatic memory management. The idea is simple: determine which variable will no longer be used, and then free up its memory. This is a cyclical process, with the garbage collector running automatically at regular intervals (or at a predetermined collection time during code execution).

Local variables in a function exist during the execution of the function. At this point, stack (heap) memory allocates space to hold the corresponding variable values. The function uses the variable internally and exits. The local variable is no longer needed and its memory can be freed. And it’s not always obvious. The garbage collector must keep track of which variables are used and which are not in order to reclaim memory. There are different implementations of how to mark unused variables. However, in the history of browsers, two markup strategies have been used: tag cleanup and reference counting.

Mark sweep

When the garbage collector runs, it marks all variables in memory (there are many ways to mark variables, such as maintaining two lists of variables in and out of context, reversing a bit of a variable when it enters the context, etc.). It then unflags all variables in the context, as well as those referenced by variables in the context. Variables that still have tags after this point can be deleted because they are no longer accessible to variables in context. The garbage collector then does a memory cleanup, destroying all marked variables and reclaiming their memory.

Today, almost all browsers use tag cleaning (or variations of it) in their JavaScript implementations, but they vary in how often they run garbage collectors.

Reference counting

No browser currently uses this markup strategy. The idea is to keep track of how many times each value is referenced (declare a variable and assign a value to it, and the number of references to that value is 1). When the number of references to a value is zero, the value is no longer useful and can be reclaimed. The next time the garbage collector runs, it frees memory for the zero reference value.

Reference counting has a serious problem: circular references. A circular reference is when object A has A pointer to object B, and object B refers to object A. Such as:

function probleFn() {
	const objA = new Object(a);const objB = {}
    objA.key = objB
    objB.key = objA
}

Copy the code

In the example, objA and objB refer to each other through their respective properties, meaning that the number of references is always 2. This is fine under the tag clearance strategy, since both objects are out of scope after the function runs and can be collected by the garbage collector. Under the reference-counting strategy, objA and objB will exist after the function ends because the number of references will never be zero. If the function is called in large numbers, a large amount of memory will never be freed. Causing memory leaks

performance

The garbage collector is executed periodically, and its scheduling strategy is important. An inappropriate scheduling strategy will not improve performance, but will significantly slow down the rendering speed and frame rate. Developers do not know when the garbage collector will be executed, so it is best to do this at code time: whenever garbage collection is started, make it finish as soon as possible, i.e. manually release (null) variables when they are used up.

Modern garbage collectors decide when to execute based on probing the JavaScript runtime environment. The detection mechanism varies from engine to engine, but is basically based on the size and number of allocated objects. For example, the V8 team’s 2016 blog post states that after a full garbage collection, V8’s heap growth strategy determines when to recycle again based on the number of active objects plus some margin.

Memory management

JavaScript runs in an environment where memory management and garbage collection are special. Browsers typically have much less memory allocated than desktop software, and mobile browsers even less. This is for safety reasons, in case a Javascript-heavy web page runs out of memory and crashes the operating system.

With limited memory, keeping only the necessary data in the executing generation results in better page performance. If the data is no longer in use, it is manually set to NULL, thereby dereferencing. This works well with global variables and global objects and their properties. Local variables are automatically dereferenced when they go out of scope.

function fn() {
  Num is automatically dereferenced when the function completes
  const num = 3
  return num
}
const fnReturn = fn()
console.log(fnReturn)
// Manually dereference global variables after they are used
fnReturn = null
Copy the code

Unapplying a value does not cause the associated memory to be freed automatically. The key to dereferencing is to ensure that the value is no longer in context, so it will be collected in the next garbage collection.

Improve performance with const and let declarations

Const and let variables are block-scoped. In cases where block-level scope terminates earlier than function scope, variables declared with these two keywords allow the garbage collector to intervene earlier and reclaim the associated memory earlier than var.

Hide classes and delete operations

The dynamic nature of JavaScript tends to undermine the browser’s “hidden class” optimization strategies, such as dynamically adding and removing properties.

V8 makes use of “hidden classes” when compiling interpreted JavaScript code into machine code. At run time, V8 associates the created objects with the hidden classes to track their attribute characteristics. Objects that can share the same hidden class perform better.

function Article() {
  this.title = 'test'
}

const obj1 = new Article()
const obj2 = new Article()
Copy the code

V8 will configure the two instances of obj1 and obj2 in the background to share the same hidden class because both instances share the same constructor and stereotype.

The hidden class is an anonymous class declared behind the scenes by V8 itself, in conjunction with its fast access mode to improve performance. For details, please refer. Currently, it can be simply understood as a class

If the following line had been added at this point:

obj2.author = 'Jack'
Copy the code

At this point, the two Article instances correspond to two different hidden classes (one more class). If the frequency of such operations is high and the ratio of hidden classes is large, it can have a noticeable impact on performance. It also breaks the optimization that the fast access mode brings (degradation from static typing to dynamic typing). Using DELETE to dynamically remove attributes poses the same problem.

The best practice is to declare all attributes at once in the constructor to avoid creating and then adding attributes. Where delete.key is required, set it to null manually, for example, obj.title = null.

A memory leak

Memory leaks in JavaScript are mostly caused by improper references, such as:

  • Accidentally declared global variables

    function fn() {
      t = 'test'
    }
    Copy the code

    Since t is not declared with const, let, or var keywords, it exists as an attribute of the Window object. Originally declared as a local variable, t is now a global variable that can be terminated until the application exits.

  • Timers can also cause memory leaks, such as when the timer’s callback refers to an external variable via encryption

    function fn() {
      const t = 'test'
      setInterval(() = > {
        console.log(t)
      }, 1000)
    }
    fn()
    Copy the code

    The timer that runs once for 1s causes the local variable t to be unable to be released after the function is finished

  • Closures can unwittingly cause memory leaks

    let outer = function(){
      const t = 'test'
      return function() {
        console.log(t)
      }
    }()
    
    // If this line is not executed, the memory of variable t will never be reclaimed
    // outer = null
    Copy the code

    The presence of outer causes the memory space of the function’s local variable T to never be freed because the closure keeps referring to it.

The points above may seem small, but when you zoom in on the amount of data involved, the problem becomes much bigger than you might think.

Static allocation with object pooling

By using allocated memory wisely, you avoid unwanted garbage collection, thereby preserving performance lost by freeing memory.

One of the criteria the browser uses to decide when to run the garbage collector is the speed at which objects are replaced. If a lot of objects are initialized and then all of a sudden go out of scope, the browser will schedule the garbage collector more aggressively, and some of the performance will be lost due to the large number of garbage collector runs.

The strategy is to manage a collection of recyclable objects through a pool of objects. An application can request an object from this object pool, set properties, use it, and then return it to the object pool when the operation is complete. Since there is no object initialization or destruction, garbage collection cannot detect object replacement, and therefore garbage collection is not executed frequently.

Using arrays to maintain object pools is a good option, but be careful not to incur additional garbage collection, such as:

const objArr = new Array(100)
// Suppose that 100 objects have been stored
const obj = new Object(a)// There will be additional garbage collection
objArr.push(obj)
Copy the code

Because the size of JavaScript arrays is dynamically variable, the engine deletes the original 100 array and then creates a new 200 array. The garbage collector sees this deletion and soon comes to pick up the garbage. So, to avoid this dynamic allocation, you can create an array of sufficient size at initialization to avoid additional garbage collection.

Static allocation is an extreme form of optimization. If your application is severely hampered by garbage collection, use it to improve performance. But this is rare, and in most cases, it’s premature optimization, so don’t worry about it.

link

  • JavaScript knowledge you must Understand – Basics

  • JavaScript you must understand — variables, scopes, memory

  • JavaScript you must understand — reference types