Home >Web Front-end >JS Tutorial >In-depth understanding of JS underlying mechanisms such as JS data types, precompilation, and execution context

In-depth understanding of JS underlying mechanisms such as JS data types, precompilation, and execution context

WBOY
WBOYforward
2021-12-27 18:48:522478browse

JavaScript consists of three parts: the Document Object Model DOM, the Browser Object Model BOM, and its core ECMAScript. This article brings knowledge of the underlying principles in JavaScript and hopes to be helpful to everyone.

In-depth understanding of JS underlying mechanisms such as JS data types, precompilation, and execution context

JavaScript is a literal interpreted scripting language that is dynamic, weakly typed, and prototype-based. JavaScript is rooted in the web browser we use, and its interpreter is the JavaScript engine in the browser. This scripting language that is widely used on the client side was first used to handle some input validation operations that were previously handled by server-side languages. With the development of the Web era, JavaScript has continued to grow and become a fully functional programming language. Its use is no longer limited to simple data validation, but has the ability to interact with almost all aspects of the browser window and its content. It is both a very simple language and an extremely complex language. If we want to be truly proficient in JavaScript, we must have an in-depth understanding of some of its underlying design principles. This article will refer to the "JavaScript Advanced Programming" and "JS You Don't Know" series of books to explain some underlying knowledge about JavaScript.

Data type

According to the storage method, JavaScript data types can be divided into two Two types, primitive data types (primitive values) and reference data types (reference values).

There are currently six primitive data types, including Number, String, Boolean, Null, Undefined, and Symbol (ES6). These types are actual values ​​stored in variables that can be directly manipulated. Primitive data types are stored on the stack, and the data size is determined. They are stored directly by value, so they can be accessed directly by value.

The reference data type is Object. In JavaScript, everything except primitive data types are Object types, including arrays, functions, regular expressions, etc., which are all objects. A reference type is an object stored in heap memory, and a variable is a reference address stored in stack memory that points to an object in heap memory. When a variable is defined and initialized to a reference value, if it is assigned to another variable, the two variables save the same address and point to the same memory space in the heap memory. If you modify the value of a reference data type through one of the variables, the other variable will also change accordingly.

For primitive data types, except null, which is special (null will be considered an empty object reference), for others we can use typeof to make accurate judgments:

##'object'typeof undefined'undefined'typeof unknownVariable('undefined'typeof Symbol()##typeof []'object'##typeof(/[0-9,a-z]/) (

Expression

Return value

##typeof 123

'number'

typeof "abc"

'string'

typeof true

##'boolean'

typeof null

Undefined variable)

'symbol'

##typeof function() {}

'function'

##typeof {}

'object'

'object'

## For null types, you can use the equality operator Make judgments. A declared but uninitialized variable value will be assigned to undefined by default

can also be assigned to undefined manually), in JavaScript, use The equality operator == cannot distinguish between null and undefined, and ECMA-262 stipulates that their equality test must return true. To accurately distinguish two values, you need to use the congruence operator ===. For reference data types, except function, which is special in method design and can be accurately judged with typeof, all others return the object type. We can use instanceof to judge reference type values. instanceof will detect whether an object A is an instance of another object B. At the bottom level, it will check whether object B exists on the prototype chain of object A

(

Instances and prototype chains will be discussed later in the article). Returns true if present, false if absent.
##Expression

##Return value 'true''true'##{name:”Alan”,age:”22”} instanceof Object'true'

[1,2,3] instanceof Array

'true'

function foo(){ } instanceof Function

##'true'

##/[0-9,a-z]/ instanceof RegExp

##new Date() instanceof Date

Since all reference type values ​​are instances of Object, use the instance operator to judge them as Object, and the result will also return true.

##new Date() instanceof Object'true'

Of course, there is a more powerful method that can accurately determine any data type in any JavaScript, and that is the Object.prototype.toString.call() method. In ES5, all objects (native objects and host objects) have an internal property [[Class]], whose value is a string that records the type of the object. Currently includes "Array", "Boolean", "Date", "Error", "Function", "Math", "Number", "Object", "RegExp", "String", "Arguments", "JSON", "Symbol". This internal property can be viewed through the Object.prototype.toString() method, there is no other way.

When the Object.prototype.toString() method is called, the following steps will be performed: 1. Get the [[Class]] attribute value of this object( I will talk about this object later in the article). 2. Place the value between the two strings "[object" and "]" and concatenate them. 3. Return the spliced ​​string.

When the value of this is null, the Object.prototype.toString() method directly returns "[object Null]". When the value of this is undefined, "[object Undefined]" is returned directly.

Expression

Return value

[1,2,3] instanceof Object

'true'

function foo(){ } instanceof Object

##'true'

##/[0-9,a-z]/ instanceof Object

'true'

##Object.prototype.toString.call(true)[object Boolean]Object.prototype.toString.call(null)[object Null]##Object.prototype.toString.call(undefined)##Object.prototype.toString.call (Symbol())Object.prototype.toString.call(function foo(){})[object RegExp]

Expression

Return value

Object.prototype.toString.call(123)

##[object Number]

Object.prototype.toString.call(“abc”)

##[object String ]

[object Undefined]

[object Symbol]

##[object Function]

Object.prototype.toString.call([1,2,3])

[object Array]

##Object.prototype.toString.call({name:”Alan” })

[object Object]

##Object.prototype.toString.call(new Date())

[object Date]

##Object.prototype.toString .call(RegExp())

Object.prototype.toString.call(window.JSON)

[object JSON]

Object.prototype.toString.call(Math)

[object Math]


The call() method can change the point of this when calling the Object.prototype.toString() method so that it points to the object we pass in, so we can get the [[Class]] attribute of the object we pass in. (Using Object.prototype.toString.apply() can also achieve the same effect) .

JavaScript data types can also be converted. Data type conversion is divided into two methods: explicit type conversion and implicit type conversion.

The methods that can be called for display type conversion include Boolean(), String(), Number(), parseInt(), parseFloat() and toString() (null and undefined values ​​do not have this method). Their respective uses are clear at a glance, so I will not introduce them one by one here.

       Since JavaScript is a weakly typed language, when using arithmetic operators, the data types on both sides of the operator can be arbitrary, unlike Java or C language Specify the same type, and the engine will automatically perform implicit type conversion for them. Implicit type conversion is not as intuitive as explicit type conversion. There are three main conversion methods:
1. Convert the value to a primitive value: toPrimitive()

2. Convert the value to a number: toNumber( )

3. Convert the value to a string: toString()

Generally speaking, when adding numbers and strings, the numbers will be converted into strings; When performing truth value judgment (such as if, ||, &&), the parameters will be converted into Boolean values; when performing comparison operations, arithmetic operations, or auto-increment and decrement operations, the parameters will be converted into Number values; when the object needs to be implicitly During type conversion, the return value of the object's toString() method or valueOf() method will be obtained.

About NaN:

NaN is a special numerical value, representing a non-numeric value. First, any arithmetic operation involving NaN will return NaN. Second, NaN is not equal to any value, including NaN itself. ECMAScript defines an isNaN() function, which can be used to test whether a parameter is "non-numeric". It first attempts to implicitly convert the argument to a numeric value, returning true if it cannot be converted to a numeric value.


We can first use typeof to determine whether it is Number type, and then use isNaN to determine whether the current data is NaN.

About strings:

Strings in JavaScript are immutable. Once strings are created, their values ​​cannot be changed. To change the string held by a variable, first destroy the original string and then fill the variable with another string containing the new value. This process happens in the background, and is why some older browsers are very slow when concatenating strings.

In fact, in order to facilitate the operation of basic type values, ECMAScript also provides three special reference types: Boolean, Number and String. Primitive data types have no properties and methods. When we call methods on primitive type values ​​to read them, the access process will be in a read mode, and a corresponding primitive wrapper type object will be created in the background, allowing us to Call some methods to manipulate this data. This process is divided into three steps: 1. Create an instance of the original packaging type 2. Call the specified method on the instance 3. Destroy the instance.
The main difference between reference types and primitive packaging types is the life cycle of the object. The automatically created primitive packaging type object only exists at the moment of execution of a line of code, and then is destroyed immediately, so we cannot use primitive type values ​​at runtime. Add properties and methods.

Precompilation

In the book "JavaScript You Don't Know", the author stated that although JavaScript is classified as a "dynamic language" or " Interpreted execution language", but in fact it is a compiled language. JavaScript execution is divided into three steps: 1. Syntax analysis 2. Precompilation 3. Interpretation and execution. Syntax analysis and interpretation execution are not difficult to understand. One is to check whether the code has syntax errors, and the other is responsible for executing the program line by line. However, the precompilation stage in JavaScript is slightly more complicated.

Any JavaScript code must be compiled before execution. In most cases, the compilation process occurs within a few microseconds before the code is executed. During the compilation phase, the JavaScript engine will start from the current code execution scope and perform an RHS query on the code to obtain the value of the variable. Then during the execution phase, the engine will execute the LHS query and assign values ​​to the variables.

During the compilation phase, part of the job of the JavaScript engine is to find all declarations and associate them with the appropriate scope. During the precompilation process, if it is in the global scope, the JavaScript engine will first create a global object (GO object, Global Object) , and promote variable declarations and function declarations. The promoted variable is first initialized to undefined by default, and the function promotes the entire function body(If it is When functions are defined in the form of function expressions, the rules for variable promotion are applied), and then they are stored in global variables. The promotion of function declarations will take precedence over the promotion of variable declarations. For variable declarations, repeated var declarations will be ignored by the engine, and subsequent function declarations can overwrite previous function declarations ( The new variable declaration syntax of ES6 is slightly different, so we will not discuss it here for now) .

The inside of the function body is an independent scope, and the pre-compilation phase is also performed inside the function body. Inside the function body, an active object (AO object, Active Object) will first be created, and the formal parameters and variables will be declared as well as the functions inside the function body. The declaration is promoted, the formal parameters and variables are initialized to undefined, the internal function is still the internal function body itself, and then they are stored in the active object.

After the compilation phase is completed, the JavaScript code will be executed. The execution process assigns values ​​to variables or formal parameters in sequence. The engine will look for corresponding variable declarations or formal parameter declarations in the scope, and if found, it will assign values ​​to them. For non-strict mode, if a variable is assigned without a declaration, the engine will automatically and implicitly create a declaration for the variable in the global environment. However, for strict mode, an error will be reported if an undeclared variable is assigned a value. . Because JavaScript execution is single-threaded, if the assignment operation (LHS query) is executed, the variable must be obtained first (RHS query) and output, you will get an undefined result, because the variable has not been assigned a value at this time.

Execution environment and scope

##Each function is a Function object For example, in JavaScript, every object has an internal property that is only accessible to the JavaScript engine - [[Scope]]. For functions, the [[Scope]] attribute contains the collection of objects in the scope in which the function was created - the scope chain. When a function is created in the global environment, the function's scope chain inserts a global object that contains all variables defined in the global scope.

The inner scope can access the outer scope, but the outer scope cannot access the inner scope area. Since JavaScript does not have block-level scope, variables defined in an if statement or a for loop statement can be accessed outside the statement. Before ES6, JavaScript only had global scope and function scope. ES6 added a new block-level scope mechanism.

When the function is executed, an execution environment called execution environment (execution context, also called execution context) will be created for the execution function. Internal object. Each execution environment has its own scope chain. When an execution environment is created, the top of its scope chain is first initialized to the object in the [[Scope]] attribute of the currently running function. Immediately afterwards, the active object when the function is running (including all local variables, named parameters, arguments parameter set and this) will also be created and pushed into effect The top of the domain chain.

The corresponding execution environment is unique each time a function is executed, and the same function is called multiple times This will result in the creation of multiple execution environments. When the function completes execution, the execution environment will be destroyed. When the execution environment is destroyed, the active object is also destroyed(The global execution environment will not be destroyed until the application exits, such as closing the web page or browser).

During the execution of the function, every time a variable is encountered, it will go through an identifier parsing process to determine where to obtain or store the data. Identifier resolution is the process of searching for identifiers level by level along the scope chain. The global variable is always the last object of the scope chain (that is, the window object).

In JavaScript, there are two statements that can temporarily change the scope chain during execution. The first is the with statement. The with statement creates a mutable object that contains all the properties of the object specified by the parameter, and pushes the object to the first position of the scope chain, which means that the active object of the function is squeezed to the second position of the scope chain. Although this makes accessing properties of mutable objects very fast, accessing local variables, etc. becomes slower. The second statement that can change the scope chain of the execution environment is the catch clause in the try-catch statement. When an error occurs in the try code block, the execution process will automatically jump to the catch clause, and then the exception object will be pushed into a variable object and placed at the top of the scope. Inside the catch code block, all local variables of the function will be placed in the second scope chain object. Once the catch clause is executed, the scope chain returns to its previous state.

Constructor

The constructor in JavaScript can be used to create objects of specific types. In order to distinguish them from other functions, constructors generally start with a capital letter. However, this is not necessary in JavaScript, because JavaScript does not have a special syntax for defining constructors. In JavaScript, the only difference between constructors and other functions is the way they are called. Any function can be used as a constructor as long as it is called through the new operator.

JavaScriptThe function has four calling modes: 1. Independent function calling mode, such as foo (arg). 2. Method calling mode, such as obj.foo(arg). 3. Constructor calling mode, such as new foo(arg). 4.call/apply calling mode, such as foo.call(this,arg1,arg2) or foo.apply(this,args) (args here is an array).

To create an instance of a constructor and play the role of a constructor, you must use the new operator. When we use the new operator to instantiate a constructor, the following steps will be performed inside the constructor:
1. Implicitly create an empty this object
2. Execute the code in the constructor (add attributes to the current this object )
3. Implicitly return the current this object

If the constructor explicitly returns an object, then the instance is returned Object, otherwise it is the this object returned implicitly.

When we call the constructor to create an instance, the instance will have all the instance attributes and methods of the constructor. For different instances created through constructors, their instance properties and methods are independent. Even if they are reference type values ​​with the same name, different instances will not affect each other.

Prototype and prototype chain

Prototype and prototype chain are the core of the JavaScript language One of the essences is also one of the difficulties of this language. Prototype prototype (explicit prototype) is a unique attribute of a function. Whenever a function is created, the function will automatically create a prototype attribute and point to the prototype object of the function. All prototype objects will automatically obtain a constructor (constructor, which can also be translated as constructor) attribute. This attribute contains a pointer to the function ## where the prototype attribute is located. # (that is, the constructor itself) pointer to . When we create an instance through the constructor, the instance will contain an internal property of [[Prototype]] (implicit prototype), which also points to the prototype object of the constructor. In Firefox, Safari, and Chrome, each object can access their [[Prototype]] properties through the __proto__ attribute. For other browsers, this attribute is completely invisible to scripts.

The prototype attribute of the constructor and the [[Prototype]] of the instance both point to the prototype object of the constructor, and the [[ of the instance There is no direct relationship between Prototype]] properties and constructors. To know whether the [[Prototype]] property of an instance points to the prototype object of a certain constructor, we can use the isPrototypeOf() or Object.getPrototypeOf() method.

Whenever a property of an object instance is read, a search is performed, targeting the property with the given name. The search first starts from the object instance itself. If an attribute with a given name is found in the instance, the value of the attribute is returned; if not found, the search continues for the prototype object pointed to by the [[Prototype]] attribute of the object. In the prototype Searches an object for a property with a given name, and returns the property's value if found.

To determine which constructor the object is a direct instance of, you can access the constructor directly on the instance Properties, the instance will read the constructor property on the prototype object through [[Prototype]] and return the constructor itself.

The values ​​in the prototype object can be accessed through the object instance, but they cannot be modified through the object instance. If we add a property in the instance with the same name as the instance prototype object, then we create the property in the instance. This instance property will prevent us from accessing that property in the prototype object, but it will not modify that property. Simply setting the instance property to null does not restore access to the property in the prototype object. To restore access to the property in the prototype object, you can use the delete operator to completely delete the property from the object instance.

Use the hasOwnProperty() method to detect whether a property exists in the instance or in the prototype . This method will return true only if the given property exists in the object instance. To obtain all enumerable instance properties of the object itself, you can use the ES5 Object.keys() method. To get all instance properties, whether enumerable or not, you can use the Object.getOwnPropertyNames() method.

The prototype is dynamic, and any modifications made to the prototype object can be immediately reflected on the instance, but if it is repeated Writing the entire prototype object, the situation is different. Calling the constructor will add a [[Prototype]] pointer to the original prototype object to the object instance. After rewriting the entire prototype object, the constructor points to the new prototype object. All prototype object properties and methods exist with the new prototype. on the object; and the object instance also points to the original prototype object, so the connection between the constructor and the original prototype object pointing to the same prototype object is severed, because they point to different prototype objects respectively.

To restore this connection, you can instantiate the object instance after rewriting the constructor prototype, or modify the object instance The __proto__ attribute repoints the constructor's new prototype object.

JavaScript uses the prototype chain as the main way to implement inheritance. It uses prototypes to let one reference type inherit the properties and methods of another reference type. The instance of the constructor has a [[Prototype]] attribute pointing to the prototype object. When we make the prototype object of the constructor equal to an instance of another type, the prototype object will also contain a [[Prototype]] pointer pointing to the other prototype. If another prototype is an instance of another type... and so on, a chain of instances and prototypes is formed. This is the basic concept of the so-called prototype chain.

The prototype chain extends the prototype search mechanism. When reading an instance attribute, the attribute will first be searched for in the instance. If the attribute is not found, the search for the prototype object pointed to by the instance [[Prototype]] will continue. The prototype object will also become an instance of another constructor. If the prototype object is not found, the search will continue. The prototype object [[Prototype]] points to another prototype object... The search process continues to search upward along the prototype chain. If the specified attribute or method cannot be found, the search process will be executed one by one to the prototype chain. It will stop at the end.

    If the prototype object of the function is not modified, all reference types have a [[Prototype]] attribute that points to the prototype object of Object by default. Therefore, the default prototype of all functions is an instance of Object, which is the fundamental reason why all custom types inherit default methods such as toString() and valueOf(). You can use the instanceof operator or the isPrototypeOf() method to determine whether a constructor prototype exists in the instance's prototype chain.

Although the prototype chain is very powerful, it also has some problems. The first problem is that the reference type value on the prototype object is shared by all instances, which means that the reference type properties or methods of different instances point to the same heap memory. Modifying the reference value on the prototype of one instance will affect all other instances at the same time. The reference value of the instance on the prototype object, which is why private properties or methods are defined in the constructor rather than on the prototype. The second problem with the prototype chain is that when we equate the prototype prototype of a constructor to an instance of another constructor, if we pass parameters to another constructor to set attribute values ​​at this time, then all the properties based on the original constructor will This attribute of the instance will be assigned the same value due to the prototype chain, and this is sometimes not the result we want.

Closure

Closure is one of the most powerful features of JavaScript. In JavaScript , closure, refers to a function that has the right to access variables in the scope of another function, which means that the function can access data outside the local scope. A common way to create a closure is to create a function inside another function and return this function.

Generally speaking, when the function is executed, the local active object will be destroyed, and only the global scope will be saved in the memory. However, the situation is different with closures.

The [[Scope]] attribute of the closure function will be initialized to the scope chain of the function that wraps it, so the closure contains A reference to an object in the same scope chain as the execution environment. Generally speaking, the active object of the function will be destroyed along with the execution environment. But when a closure is introduced, the active object of the original function cannot be destroyed because the reference still exists in the [[Scope]] attribute of the closure. This means that closure functions require more memory overhead than non-closure functions, resulting in more memory leaks. In addition, when a closure accesses the active object of the original wrapping function, it needs to first resolve the identifier of its own active object in the scope chain and find the upper layer. Therefore, the closure using the variables of the original wrapping function is also detrimental to performance. Have a great impact.

Whenever you use a callback function in a timer, event listener, Ajax request, cross-window communication, Web Workers, or any other asynchronous or synchronous task, you are actually using a closure.

A typical closure problem is to use a timer to output loop variables in a for loop:

This code, for those who are not familiar with JavaScript closures For example, you may take it for granted that the results will output 0, 1, 2, and 3 in sequence. However, the actual situation is that the four numbers output by this code are all 4.

This is because, since the timer is an asynchronous loading mechanism, it will not be executed until the for loop is traversed. Each time the timer is executed, the timer looks for the i variable in its outer scope. Since the loop has ended, the i variable in the external scope has been updated to 4, so the i variables obtained by the four timers are all 4, instead of our ideal output of 0, 1, 2, 3.

To solve this problem, we can create a new scope that wraps the immediate execution function, and save the i variable of the external scope in each loop to the newly created scope, so that the timer can execute the function every time First get the value from the new scope. We can use the immediate execution function to create this new scope:

In this way, the result of the loop execution will output 0, 1, 2 and 3, we can also simplify this immediate execution function and directly pass the actual parameter i to the immediate execution function, so there is no need to assign a value to j inside:

Of course, it is not necessary to use an immediate execution function. You can also create a non-anonymous function and execute it every time it loops, but this will take up more memory to save the function declaration.

Because there was no block-level scope setting before ES6, we can only solve this problem by manually creating a new scope. ES6 started to set block-level scope. We can use let to define block-level scope:

The let operator will create a block-level scope and declare it through let The variables are stored in the current block scope, so each immediately executed function will look up the variables from its current block scope every time.

# LET also has a special definition, which makes the variable not only declared once during the cycle. Each cycle will be re -declared, and the value of the new statement is initialized with the value of the end of the cycle. We can also use let directly at the head of the for loop:

this points to

This keyword is one of the most complex mechanisms in JavaScript. It is automatically defined in the scope of all functions. It is easy for people to understand this as pointing to the function itself. However, in ES5, this is not bound when the function is declared. It is bound when the function is run. Its pointing only depends on how the function is called. It has nothing to do with where the function is declared. (This in the new arrow function in ES6 is different, and its pointing depends on the location of the function declaration.)

Remember what I mentioned earlier Are there four function calling modes: 1.Independent function calling mode, such as foo(arg). 2.Object method calling mode, such as obj.foo(arg). 3.Constructor calling mode, such as new foo(arg). 4.call/applyCalling mode, such as foo.call(this) or foo.apply(this).

For the independent function calling mode, in non-strict mode, this in it will point to the global object by default. In strict mode, this is not allowed to be bound to the global object by default, so it will be bound to undefined.

For the object method calling mode, this in the function will point to the object itself that calls it:

For the constructor calling mode, the execution steps inside the constructor have been introduced before:
1. Implicitly create a this empty object
2. Execute the code in the constructor (add attributes to the current this object)
3. Implicitly return the current this object

Therefore, when calling a function using new method, its this points to the this object created implicitly and independently inside the constructor. All properties or methods added through this will eventually will be added to this empty object and returned to the constructor instance.

For the call/apply calling mode, this in the function will be bound to the first parameter you pass in, as shown in the figure:

                                          foo.apply() and foo.call() have the same function of changing the point pointed to by this. The only difference is that the second parameter is passed in array format or scattered. Parameter format.

About the underlying principles of JavaScript, I will temporarily write it here today. I will continue to update the content about JavaScript in the future. Welcome to continue to pay attention.

[Related recommendations: javascript learning tutorial]

The above is the detailed content of In-depth understanding of JS underlying mechanisms such as JS data types, precompilation, and execution context. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:csdn.net. If there is any infringement, please contact admin@php.cn delete