I have been engaged in front-end work for more than a year from formal learning. Personally, I attach great importance to basic learning. Since I am not a computer professional, I always feel that my foundation is not solid enough. So, buy a few relatively basic books to do a more in-depth reading and write down, starting with JavaScript Ninja Secrets ~

Due to the word limit, a new article: JavaScript Ninja Secrets “Deep Reading Records (ii)

A warm up,


Understand the JavaScript language

JavaScript functional languages have more pedigree than other major languages. Some concepts in JavaScript are fundamentally different from those in other languages. These fundamental differences include the following:

  • Functions are first-class citizens (first-class objects), and in JavaScript, functions coexist with other objects and can be used just like any other object. Functions can be created from literals, assigned to variables, passed as function arguments, or even returned from functions as return values.
  • A function closure
  • scope
  • Object oriented based on prototype

JavaScript is made up of a tight combination of objects, prototypes, functions, and closures. Understanding how these concepts are closely related can greatly improve programming skills and provide a solid foundation for developing all types of applications, whether on the web, desktop, mobile, or server. In addition to these basic concepts, there are other features of JavaScript that can help you write elegant and efficient code. There are the following features, which will be highlighted in future articles:

  • Generators, a function that generates multiple values based on a single request, can also suspend execution between requests.
  • Promise, let’s get more control over our asynchronous code.
  • Proxy, which lets us control access to specific objects.
  • Map, for creating dictionary collections; Set, which handles collections containing only non-repeating items.
  • Regular expression, simplify the code to write up very complex logic.
  • Modules, which break code into smaller, self-contained pieces to make projects more manageable.

2 Understanding the Browser

2.3 Constructor calls

To use constructor calls, you need to use the keyword new before the function call. Calling a function with the keyword new triggers the following actions:

  1. Create a new empty object.
  2. This object is passed to the constructor as the this parameter, thus becoming the function context of the constructor.
  3. The newly constructed object is the return value of the new operator

The purpose of the constructor is to create a new object, initialize it, and then return it as the constructor value. Anything that goes against both of these is not a good constructor.

As we all know, the purpose of a constructor is to initialize the newly created object, and the newly constructed object is returned as the result of the constructor call (via the new operator). But what happens when the constructor itself returns a value? Let’s explore this situation through the following examples.

    function Test() {
        this.say = function () {
            console.log('hello~')
        }
        
        return 1
    }
    
    const test = new Test()
    test.say() // 'htllo~'
Copy the code

If you execute this code, you’ll find that everything is fine. The fact that the Test function returns the simple number 1 has no significant effect on the behavior of the code. The Test function does return 1 if used as a normal function, but if called as a constructor with the new keyword, a new Test object is constructed and returned. So far, so good. But if you try to make some changes, one constructor returns another object.

    const tempTest = {
        isFlag: false
    }
    
    fucntion Test() {
        this.isFlag = true
        
        return tempTest
    }
    
    const test = new Test()
    
    cosole.log(test.isFlag) // false
    
Copy the code

The test results show that the tempTest object ends up as the return value of the constructor call, and that operations on the function context in the constructor are invalid, and that the tempTest will end up being returned. Here are some summaries of these results.

  • If the constructor returns an object, that object is returned as the value of the entire expression, and this passed to the constructor is discarded.
  • However, if the constructor returns a non-object type, the return value is ignored and the newly created object is returned.

Because of these properties, constructors are generally not written for other functions.

2.4 Call using apply and call methods

The main difference between different types of function calls is the object that is ultimately passed to the executing function as the function context (which can be implicitly referenced to by this). In the case of a method, it is the object of the method. Window or undefined for top-level functions (depending on whether they are in strict mode); For a constructor, it is a newly created object instance.

But what if you want to change the function context? What if you want to display the specified one? Js gives us a way to call functions that explicitly specify any object as the context of the function. We can do this using two methods that exist on every function: apply and Call.

Yes, we mean the formal function method. As first class objects (functions created by the built-in Function constructor), functions can have attributes like other object types, including methods.

The only difference between Apply and Call is how parameters are passed. In the case of Apply we use an array of parameters, and in the case of call we list the call parameters after the function context.

3.1 Use arrow functions to bypass function context

Arrow functions do not have a separate “this” value. The “this” value of the arrow function is the same as the context in which the declaration is made. The “this” value is determined at creation time.

When the arrow function is called, the this parameter is not passed implicitly, but the context is inherited from the function at definition.

<button id="test">Click Me! </button> <script> const button = { clicked: false, click: () => { this.clicked = true } } const elem = document.getElementById("test") elem.addEvenListener("click", </script> </script> </script> Console. log(button.clicked) // false //Copy the code

Define the object literal in the global code, and define the arrow function in the literal, so that this in the arrow function points to the global window object

3.2 Using bind

In addition to Call and apply, you can control the context and parameters of calling functions. In addition, functions can access the bind method to create new functions. Regardless of the method call, the bind method creates a new function with the same body as the original function, and the new function is bound to the specified object.

All functions have access to the bind method, which creates and returns a new function and binds it to the incoming object. Regardless of how this function is called, this is set to the object itself. The bound function has the same behavior and body as the original function.

summary


  • When you call a function, you pass in two implicit arguments: arguments and this, in addition to the arguments that display the declaration in the function definition.

    1. The arguments argument is the collection of all arguments passed to the function. The arguments argument can also be used to retrieve arguments that do not match function parameters. In non-strict mode. The Arguments object is an alias for the function arguments. Modifying the arguments object changes the function arguments, which can be avoided by strict mode.
    2. This represents the function context, which is the object associated with the function call. How the function is defined and called determines the value of this.
  • There are four ways to call a function.

    1. Call test() as a function
    2. Call obj.test() as a method
    3. Call as constructor: new Ninja()
    4. Call with the apply and call methods: test.apply(obj), test.call(obj)
  • Arrow functions do not have a separate this value, which is determined when the arrow function is created.

  • All functions can use the Bind method to create a new function and Bind it to the parameters passed in by the Bind method. The bound function has the same behavior as the original function.

5. Master functions: closures and scopes


5.1 Understand closures

  • Closures allow functions to access and manipulate variables inside other functions. Closures enable functions to access variables or functions as long as they exist in the scope in which the function was declared.
  • When you declare an inner function in an outer function, you not only define the declaration of the function, but also create a closure. This closure contains not only the declaration of the function, but also all variables in that scope at the time the function is declared. When the inner function is finally executed, the original scope is still accessible through the closure, even though the declared scope is gone.
  • Each function that accesses a variable through a closure has a scope chain that contains all of the closure’s information.
  • While closures are very useful, they should not be overused. With closures, all information is stored in memory until the Js engine ensures that it is no longer used or the page is unloaded.

5.2 Using closures

5.2.1 Encapsulating private variables

Many programming languages use private variables, which are object properties hidden from the outside world. This is a very useful feature. Because we don’t want to overload the user with the implementation details of the object when accessing these variables through other code. Unfortunately, native Js does not support private variables. But by using closures, we can achieve close, acceptable private variables, as shown in the example code below.

function Ninja() {
    var feints = 0
    this.getFeints = function() {
        return feints
    }
    
    this.feint = function() {
        feints++
    }
}

var ninja1 = new Ninja()
ninja1.feint()
Copy the code
  • The object instance is visible through the ninja1 variable.
  • Because the feint method is inside the closure, you can access the variable feints.
  • Outside the closure, we have no access to the variable feints.

By using closures, ninja’s state can be maintained by methods without direct user access — this is because variables inside the closure can be accessed by methods inside the closure, while variables inside the closure cannot be accessed by code outside the constructor.

5.3 Trace code through execution context

In Js, the basic unit of code execution is a function. We use functions all the time, we use functions to do calculations, we use functions to update our UI, we use functions to reuse code, we use functions to make our code easier to understand. To do this, the first function can call the second function, the second function can call the third function, and so on. When a function call occurs, the program returns to the location of the function call. So how does the Js engine track the execution of a function and return to its position?

As mentioned earlier, there are two types of Js code: global code, defined outside all functions, and function code, located inside functions. When the Js engine executes code, each statement is in a specific execution context. Since there are two types of code, there are two execution contexts: global execution context and function execution context. The main difference between the two is that there is only one global execution context. The global execution context is created when the Js program starts executing, while the function execution context is created every time the function is called.

Note: We said earlier that the function context can be accessed through the keyword this. Function execution context, although also called context, is an entirely different concept. An execution context is an internal Js concept that the Js engine uses to track the execution of a function.

Js is a single-threaded execution model: only certain code can be executed at any given time. Once a function call occurs, the current execution context must stop execution and a new function execution context must be created to execute the function. The function execution context is destroyed when the function is finished and returned to the execution context in which the call was made. So you need to track the execution context – the context in which you are executing and the context in which you are waiting. The simplest way to trace is to use an execution up and down stack (or call stack).

A stack is a basic data structure where data items can be inserted and read only at the top of the stack. This characteristic can be generalized to a stack of trays in a cafeteria. You can only take one tray from the top of the stack, and the waiter can only put a new tray on top of the stack.Copy the code

Let’s look at a piece of code:

function skulk(ninja) {
    report(ninja + ' skulking')
}

function report(message) {
    console.log(message)
}

skulk('Kuma')
skulk('Yoshi')
Copy the code

This code is relatively simple, starting with the definition of shulk, which calls the report function. The skulk function is then called globally twice: skulk(‘Kuma’) and skulk(‘Yoshi’). With this basic code, we can explore how the execution context is created. As shown below:

  1. Each Js program creates only one global execution context and executes from the global execution context (in single-page applications, there is only one global execution context per page). When global code is executed, the global execution context is active.
  2. First define two functions in the global code: shulk and report, and then call skulk(‘Kuma’). Since only certain code can be executed at any given time, the Js engine stops executing the global code and starts executing the skulk function with Kuma arguments. Create a new function execution context and place it at the top of the execution context stack.
  3. The skulk function then calls the report function. Again, because only certain code can be executed at any given time, skulk execution context is paused, a new report context is created with Kuma as an argument, and placed at the top of the execution context stack.
  4. After report prints a message via the built-in function console.log, the report function completes and the code returns to skulk. The report execution context pops from the top of the execution context stack, the skulk function execution context is reactivated, and the skulk function continues execution.
  5. A similar thing happens after skulk completes: the skulk function execution context pops off the top of the stack, reactivates the waiting global execution context and resumes execution. Js global code resumes execution.

5.4 Use lexical context to track the scope of variables

The lexical environment is used internally by the Js engine to track the mapping between identifiers and specific variables. Lexical environments are the internal implementation mechanism of Js scopes, and people are usually called scopes.

In general, the lexical context is associated with a particular JS structure, either as a function per code snippet or as a try-catch statement. These code structures can have separate identifier mapping tables.

5.4.1 Code nesting

Lexical environments are primarily based on code nesting, through which one code structure can contain another. Each time the code is executed, the code structure gets the lexical environment associated with it within the scoped scope. For example, each time a function is called, a new function lexical environment is created.

It is also important to emphasize that internal code structures can access variables defined in external code structures. How does the Js engine keep track of these variables? How do you determine accessibility? That’s where the lexical environment comes in.

5.4.2 Code nesting and lexical environment

  • In addition to tracking local variables, function declarations, function parameters, and the lexical environment, it is also necessary to track the external (parent) lexical environment. Because we need to access variables in the external code structure, if we cannot find an identifier in the current environment, the external environment is looked up. The search is stopped once a matching variable is found, or an error is returned when the identifier is still not found in the global environment.
  • Whenever a function is created, a lexical Environment is created associated with it and stored on an internal property named [[Environment]] (that is, not directly accessible or manipulated). Two brackets are used to mark internal attributes.
  • Whenever a function is called, a new execution environment is created and pushed up and down the execution stack. In addition, a lexical environment associated with it is created. For now, the most important part is the external Environment and the new lexical Environment. The Js engine associates the internal [[Environment]] property of the calling function with the Environment in which the function was created.

5.5 Understand JavaScript variable types

In Js, we can define variables with three keywords: var,let, and const. These three keywords differ in two ways: variability and relationship to lexical context (scope).

Note that the var keyword was originally part of Js, while let and const were added in ES6.

5.1 Variable variability

A “variable” declared by const is like a normal variable, but requires an initial value at declaration time. Once declared, its value cannot be changed. It sounds immutable, right? Const variables are often used for two purposes:

  • Special variables that do not need to be reassigned.
  • Pointing to a fixed value, such as the maximum number of team members, can be represented by the const MAX_RONI_COUNT variable instead of just the number 234. This makes the code easier to understand and maintain.
  • Reassignment of const variables is not allowed during program execution to avoid unnecessary code changes and to facilitate JavaScript engine performance optimization.

Here we test the variability of a variable declared by the const keyword:

  1. Test ++ = test+ 1; test++ = test+ 1; test is static and cannot be reassigned.
  2. Next we define a testObj variable and initialize it with an empty object. We add a new attribute name to the empty object and find that no exception is thrown.
  3. We then define an empty array, testArr, and push new values and operation lengths into the empty array without throwing an exception
  4. Finally, the array is reassigned to an empty array, and the Js engine throws an exception.

Conclusion: This is all about const variables. Const variables can only be initialized once at declaration time and can never be assigned a new value to a const variable. We can modify the value of a const that already exists, but we cannot override const. For example, we cannot assign a new value to a const variable, but we can modify an existing object of a const variable by adding attributes to an existing object.

5.5.2 Define the keywords and lexical environment of variables

  • Unlike var, which defines variables in the nearest function or global lexical context, let and const are more straightforward. Let and const define variables directly in the nearest lexical context (either block-level scope, loop, function, or global context). We can use let and const to define block-level, function-level, and global-level variables. Note: When defining global variables using const, global static variables are usually represented in uppercase.

5.5.3 Registering identifiers in a lexical environment

As a programming language, THE basic principle of Js design is ease of use. This is the main reason you don’t need to specify function return value types, function parameter types, variable types, and so on. We’ve seen that JS is executed line by line. Check out the following code:

firstRonin = 'Kiyokawa'
secondRoin = 'Kondo'
Copy the code

Assign Kiyokawa to the identifier firstRonin and Kondo to the identifier secondRonin. There doesn’t seem to be anything special. Let’s look at another piece of code:

const firstRonin = 'Kiyo'

check(firstRoin)

function check(roin) {
    console.log(roin === 'Kiyokawa')
}
Copy the code

In the above code, we assign Kiyokawa to firstRonin and then call the check function, passing in the firstRoin argument. If Js is executed line by line, can we call check at this point? The program has not executed the declaration of check, so the Js engine should not recognize check.

However, the program ran smoothly. Js is not picky about where functions are defined. Before or after the call. Why is that? How does the Js engine know that check exists? This shows that the Js engine is playing a trick and that the execution of the Js code actually takes place in two stages. Once the new lexical environment is created, the first phase is performed. In the first phase, no code is executed, but the JS engine accesses and registers variables and functions declared in the current lexical environment. Once the first phase is complete, the second phase is executed, depending on the type of variable and the environment type. The specific processing process is as follows:

  1. Create default values for parameters and function parameters if you are creating a function environment. Skip the step if you are creating a non-function environment.
  2. If you are creating a global or function environment, scan the current code for function declarations (not the body of other functions), except function expressions and arrow functions. For the function declaration found, the function is created and bound to the same identifier as the function name in the current environment. If the identifier already exists, the value of the identifier will be overridden. If it is a block-level scope, this step is skipped.
  3. Scan the current code for variable declarations. In a function or global environment, find all variables declared by var outside the current function and other functions, and find all variables defined by let or const. In a block-level environment, only variables defined by let or const in the current block are found. For variables found in the current execution environment, if the identifier does not exist, register and initialize it as undefined. If the identifier already exists, its value is retained.
  • If a function is defined as a function declaration, it can be accessed before the function declaration
  • If a function is defined by a function expression or an arrow function, it cannot be accessed before the function is defined.

Function overloading

One challenge is dealing with overloaded function identifiers. Let’s start with a piece of code.

console.log(typeof fun === 'function'); // true var fun = 3; console.log(typeof fun === 'number'); // true function fun(){} console.log(typeof fun === 'number') // true fun still points to numbersCopy the code
  • In the above code, the declared variables and functions use the same name fun. If you execute this code, you’ll see that all three console. logs print true.

  • This behavior of Js is directly caused by the result of identifier registration. As we mentioned earlier, in step 2 of the process, a function defined through a function declaration is created and assigned to the corresponding identifier before execution. In step 3, you handle declarations of variables that are not declared in the current environment and are assigned the value undefined.

  • In the above code, since the function declaration is declared before the variable declaration, the function with the identifier fun is not reassigned to undefined, so the first console.log prints true. In this case the identifier fun is a function.

  • We then execute the assignment statement var fun = 3, assigning the number 3 to the identifier fun. After executing the assignment, fun no longer refers to the function, but to the number 3.

  • The function declaration part is skipped during the actual execution of the program, so the declaration of the function does not affect the value of the identifier Fun.

Variable ascension

I’m sure you’ve all encountered this term: variable promotion: the declaration of a variable is promoted to the top of a function, and the declaration of a function is promoted to the top of the global code. However, as we saw in the above example, it is not that simple, and the declarations of variables and functions do not actually move. Just register in the lexical environment before the code executes. We get a deeper understanding of the whole process by looking at the lexical context to see what really works.

5.7 summary

  • Closures provide access to all of the variables in the environment in which the closure was created. Closures are functions and variables in the scope in which the function was created, creating “security bubbles”. In this way, even if the scope in which the function was created disappears, the function still gets everything it needs to execute

  • We can use these advanced features of closures:

    1. The private properties of an object are simulated by the variables and constructors inside the constructor.
    2. Handle callback functions to simplify code.
  • The Js engine tracks the execution of functions by performing up and down stacks (call stacks). Each time a function is called, a new function context is created and pushed to the top of the call stack. When the function completes, the corresponding execution context is pushed out of the call stack.

  • The Js engine tracks identifiers through lexical context (commonly known as scope)

  • In Js, we can define variables at the global level, function level and even block level.

  • We can define variables using the keywords var, let, and const:

    1. The keyword var defines the nearest function-level variable or global variable.
    2. The let keyword and const define variables at the nearest level, including block-level variables. In addition, variables defined by const can only be assigned once.
  • Closures are a side effect of the JS scope rule that allows you to call the function and access the scope in which the function was created when the scope disappears.

Future functions: Generators and Promises

In the previous chapters, we focused on functions, especially how to define them and how to use them effectively. We’ve already covered some of ES6’s features, such as arrow functions and block scopes. This chapter explores two new ES6 cutting edge features: Generators and Promises.

6.1 Using generator Functions

A generator function is an almost entirely new type of function that is completely different from standard, ordinary functions. A generator function can generate a sequence of values, but each value is generated on a per-request basis, not immediately like a standard function. We must explicitly request a new value from the generator, which then either responds with a newly generated value or tells us that it will never regenerate a new value. Even more curious, every time a generator function generates a value, it does not stop executing like a normal function. In contrast, generators almost never hang. Later, when a request for another value arrives, the generator resumes execution from where it left off last time.

Take a look at the following example:

Function * arrayGenerator () {// Define generator function yield 'test1' by adding an asterisk * after function; yield 'test2'; // Use the new keyword yield to generate a separate value yield 'test3'; } for( let weap of WeaponGenerator() ) { console.log(weap) }Copy the code

The example begins by defining a generator that can generate a series of data. Creating a generator function is as simple as adding an * asterisk after the function keyword. This allows the new yield keyword to be used inside the generator function to generate independent values.

We’ll see that there’s no return statement in the body of the WeaponGennerator. Like a standard function, it should return undefined. But the truth is that generator functions are very different from standard functions. For starters, calling a generator does not execute the generator function; instead, it creates an object called an iterator. Let’s continue exploring this object.

6.2.1 Control of generators through iterator objects

Calling a generator function does not necessarily execute the generator function body. You can communicate with generators by creating iterator objects. For example, a value that satisfies a condition can be requested through an iterator object. Modify the previous example slightly to see how the iterator object works:

Function * repeater () {yield 'katana'; yield 'wakizashi'; } // Call the generator to get an iterator, thereby controlling the implementation of the generator // Call the iterator next method to get a new value from the generator const result1 = weaponsiterator.next (); Const result2 = weaponsiterator.next (); const result2 = weaponsiterator.next (); const result2 = weaponsiterator.next (); const result3 = weaponsIterator.next(); // When there is no executable code, the generator returns undefined, indicating that its state is complete.Copy the code

Result1 Result2 Result3 The following figure shows the displayed result

As you can see from the above code and print, when the generator is called, an iterator is created.

  1. Iterators are used to control the execution of generators. The most basic interface method exposed by iterator objects is the Next method. This method can be used to request a value from the generator to control the generator.
  2. After the next function is called, the generator starts executing the code, and when the code reaches the yield keyword, it generates an intermediate result (an item in the sequence of values), and then returns a new object. It encapsulates the return result and a completion indicator.
  3. Each time a current value is generated, the generator will non-block pending execution, patiently waiting for the next requested value to arrive. This is a powerful feature that ordinary functions do not have at all.

Iterate over an iterator

By calling the iterator from the generator, exposing a next method allows us to request a new value from the iterator. The next method returns an object with the generated value. The other property included in this object, done, also tells us whether the generator can append generated values.

Now we take advantage of this principle and try iterating over the sequence of values generated by the generator with a normal while loop, as shown in the following code:

Function * TestGenerator() {yield 'HELLO1' yield 'HELLO2'} const testIterator = TestGenerator() Use this variable to hold the value generated by the generator let item while(! ((item = testIterator.next()).done)) { console.log(item) }Copy the code

In each iteration we retrieve a value from the generator using the next method of the iterator testIterator and store the value in the item variable. As with all objects returned by Next, the object referenced by the item variable contains a value attribute for the generator and a done attribute indicating whether the generator has completed generating the value. If the generator is not finished, we proceed to the next iteration, and if not, we stop the loop.

This is how the for-of loop works in the first generator example. A for-of loop is just syntactic sugar for iterating over an iterator.

6.2.4 Explore the internal composition of the generator

We’ve seen that calling a generator doesn’t actually execute it. Instead, it creates a new iterator through which we can request values from the generator. After the generator generates a value, the generator suspends execution and waits for the next request to arrive. In a way, the generator works more like a small program, a state machine moving in states. Let’s take it a step further and look at how generators follow execution context.

function* TestGenerator() {
	yield 'hello1'
    yield 'hello2'
}
Copy the code
const testIterator = TestGenerator()
Copy the code

Create generator functions from the suspended state

const result = testIterator.next()
Copy the code

Activate the generator from the suspended state to the execution state until the yield’hello1′ statement terminates and the suspended assignment state returns a new object {value: ‘hello1’, done: false}

const result2 = const result = testIterator.next()
Copy the code

Reactivate the generator from the pending assignment state to the execution state until the yield ‘hello2’ statement is aborted and the pending assignment state is returned to the new object {value: ‘hello1’, done: false}

const result3 = const result = testIterator.next()
Copy the code

Reactivate the generator from the pending transfer state to the execution state, no code can be executed, switch to the completed state, return a new object {value: undefined, done: true}

Trace generator functions by executing the context

In the previous example, we introduced the execution environment context. It is an internal JS mechanism for tracking the execution of functions. Generators are still functions, though somewhat peculiar, so let’s take a closer look at how they relate to the context of the execution environment.

6.3 the use of promise

Writing code in JS relies heavily on asynchronous computation, calculating values that we don’t need now but might need at some point in the future. So ES6 introduces a new concept that makes it easier to handle asynchronous tasks: promises.

Promise objects are placeholders for values we don’t have yet but will get in the future, a guarantee that we will eventually know the results of asynchronous computations. If we do what we promised, the result will be a value. If something goes wrong, the result is a mistake, an excuse for not delivering. One of the best examples of using promises is getting data from a server: we want to promise that we’ll get the data eventually, but there’s always the possibility that something will go wrong.

// Use the built-in Promise constructor to create a Promise object, Const testPromise = new Promise(resolve,reject) const testPromise = new Promise((resolve, reject) Reject)=>{resolve('success') // reject('fail')}) Call the second testPromise.then(res => console.log(res), err => console.log(err))Copy the code

6.3.1 Problems caused by simple callback functions

  1. Errors are difficult to handle, and try catches cannot handle asynchronous errors
  2. Performing successive steps is tricky, with a bunch of nested callback functions

6.3.2 Study Promise in depth

The Promise object is used as a placeholder for the result of an asynchronous task. It represents a value that we don’t have yet but hope to have in the future. For this reason, when a Promise object starts in a pending state, we know nothing about the promised value. So a promise object with a pair of wait states is also called an unfulfilled promise. During program execution, if the promise’s resolve function is called, the promise enters the completed state, where we can successfully retrieve the promised value.

On the other hand, if the Promise’s Reject function is called, or if an unhandled exception occurs during the promise call, the promise goes into a reject state, where we can’t get the promise’s value, but at least we know why. Once a promise goes into a completed or rejected state, it can’t switch states.

Let’s take a closer look at what happens when we use promises:

console.log('code start')

const delayPromise = new Promise((resolve, reject) => {
  console.log('delayPromise execute')

  setTimeout(() => {
    console.log('resolve delayPromise')

    resolve('hello1')
  }, 500)
})

console.log('created delayPromise')

delayPromise.then(res => {
  console.log(res)
})

const immediatePromise = new Promise((resolve, reject) => {
  console.log('immediatePromise execute')

  resolve('hello2')
})

immediatePromise.then(res => {
  console.log(res)
})

console.log('code end')
Copy the code

Output in sequence:

code start

delayPromise execute

created delayPromise

immediatePromise execute

code end

hello2

resolve delayPromise

hello1

After a delayPromise is created, there is still no way to know what the final value will be, or to guarantee that the promise will successfully enter the completed state.

So after the constructor call, delayPromise goes into a wait state. The then method of delayPromise is then called, which is used to establish a callback function that is expected to be executed after the promise is successfully implemented. Let’s go ahead and create another Promise — immediatePromise, which immediately completes the promise by calling the promise’s resolve function during the object construction phase. Unlike the delayPromise object, which entered the wait state after construction, the immediatePromise object completed the construction of the object in the solved state, so the Promise object already received the value hello2.

Then, using the then method of the immediatePromise, we registered a callback function for it to be invoked after the Promise was successfully resolved. However, now that the promise has been resolved, will the success callback be called immediately? The answer is no.

Promises are designed to handle asynchronous tasks, so the Js engine often anticipates Promise behavior with asynchronous processing. Js handles a Promise by calling the then callback after all the code in the loop has executed. So we first see the ‘code end’ printed, then the result of the immediatePromise returned, and finally after 500ms, the result of the delayPromise returned as well.

6.3.3 refused to promise

There are two ways to reject a promise: an explicit rejection, in which a promise’s executor calls the incoming Reject method, or an implicit rejection, in which an exception is thrown in the process of processing a promise. Let’s explore the process.

Explicitly rejected

// The promise can be explicitly rejected by calling the passed reject function. Reject) => {reject('reject a promise')}) Promise.then (()=>{}, err => console.log(err))Copy the code

You can explicitly reject a promise by calling the incoming Reject function. If the promise is rejected, the second callback function err is always called.

The chain calls the catch method

promise.then(()=>{})
	   .catch(err => console.log(err))
Copy the code

Implicit refused to

If an exception is encountered during execution, a promise can be rejected implicitly as well as explicitly (by calling reject).

Const promise = new promise ((resolve, reject) => {count++})Copy the code

Inside the promise function, we try to increment the variable count, which is not defined in the program. As expected, the program raised an exception, and since there was no try-catch statement in the execution function, the current promise was implicitly rejected. This rejection is also like an explicit rejection, and the catch callback is called. If you take the error callback as the second argument to the THEN function, the result is the same.

It’s pretty easy to handle errors in promises this way. Regardless of how a promise is rejected, whether it is explicitly rejected or implicitly called, all errors and rejection reasons are identified in the rejection callback whenever an exception occurs. This feature greatly eases our work.

6.3.4 Waiting for Multiple Promises

In addition to handling interdependent sequences of asynchronous tasks, promises can also significantly reduce the amount of code required to wait for multiple independent asynchronous tasks.

Use promise.all to wait for multiple promises

console.time() function getData1() { return new Promise(resolve => { setTimeout(() => { resolve('hello1') }, 100) }) } function getData2() { return new Promise(resolve => { setTimeout(() => { resolve('hello2') }, 500) }) } function getData3() { return new Promise(resolve => { setTimeout(() => { resolve('hello3') }, 800)})} // Promise. All takes an array of promises and creates a new Promise object. If promises are successful, the Promise is successful. All ([getData1(), getData2(), getData3()])). Then (results => {// The result will be an array of all promise success values. Each entry in the array corresponds to the corresponding entry in the Promise array console.log(results) console.timeend ()})Copy the code

[‘hello1’, ‘hello2’, ‘hello3’] default: 815.734ms

Promise.all method waits for all promises in the list. But if we only care about the first successful (or failed) promise, consider the promise.race approach.

6.3.5 promise competition

Promise.race([getData1(), getData2(), getData3()]).then(result => {
  console.log(result)

  console.timeEnd()
})
Copy the code

Hello1 default: 111.448ms

Using the promise.race method and passing in an array of promises returns a brand new Promise object, and as soon as one of the array’s promises is processed or rejected, the returned Promise will be processed or rejected as well.

Async function

By using the keyword async before the keyword function, you can indicate that the current function depends on a value returned by an async. At each location where an asynchronous task is invoked, an await keyword is placed to tell the JS engine to wait for the result of execution at that location without blocking the execution of the application.

6.5 summary

  • A generator is a function that does not print all sequences of values at the same time, but instead generates values based on each request.

  • Unlike standard functions, generators can suspend and restore their execution state. When the generator generates a value, it suspends execution without blocking the main thread and then quietly waits for the next request.

  • Generators are defined by an asterisk (*) after function. Within the generator function body, we can use the new yield keyword to generate a value and suspend the generator execution. If we want to transfer to another generator, we can use the yield operator.

  • As we control generator execution, it creates an iterator object by calling a generator using the iterator’s next method. In addition, we can pass values to the generator through the next function.

  • A promise is a placeholder for the computed value, a guarantee that we will eventually get an asynchronous computed result. Promises can fail as well as succeed. Once they are made, they cannot be changed.

  • Promises significantly simplify how we deal with asynchronous code. By using the THEN method to generate the promise chain, we can easily handle asynchronous timing dependencies. It is equally easy to execute multiple asynchronous tasks in parallel: just use the promise.all method.

Object orientation and prototyping

7.1 Understanding prototypes

In the process of software development, we want to reuse code as much as possible in order to avoid repeating the wheel. Inheritance is a way of code reuse. Inheritance helps to organize program code properly and extend the properties of one object to another object. In Js, inheritance can be implemented through prototypes.

The concept of a prototype is simple. Each object contains a reference to the stereotype, and when a property is looked up, it is looked up to see if the property is present on the stereotype if the object does not have the property itself.

const obj1 = { isFlag1: true } const obj2 = { isFlag2: true } const obj3 = { isFlag3: Log ('isFlag1' in obj1) // true console.log('isFlag2' in obj1) // false // object.setPrototypeof () method, SetPrototypeOf (obj1, obj2) console.log('isFlag2' in obj1) // isFlag2 is now accessibleCopy the code

When accessing properties that do not exist on an object, the prototype of the object is queried. Here, we can access the properties of Obj2 through the object obj1, since Obj2 is the prototype of Obj1. Each object can have a prototype, each object’s prototype can have a prototype, and so on, forming a prototype chain. The search for a particular attribute is delegated to the entire prototype chain, stopping only when there are no more prototypes to look for.

Note: There is no formal method for directly accessing the prototype object of an object — the “connection” in the prototype chain is defined in an internal property represented by [[prototype]] in the JavaScript language standard. However, most modern browsers do provide an attribute called proto (with two underscores on each side), which contains the prototype of the object. Obj. Proto obj. Constructor. The prototype Object. The getPrototypeOf (obj) among the above three methods, the first two are not very reliable. The latest ES6 standard states that the __proto__ attribute must be deployed only in browsers and not in other environments. The obj. Constructor. The prototype in manual change the prototype object, may fail.

7.2 Object constructors and prototypes

Js initializes the new object via the new operator via the constructor, but there is no real class definition. Trigger the creation of a new object assignment via the operator new, applied before the constructor.

Function Test() {} // Each function has a selectable prototype object, Test.prototype.say = function () {console.log('hello')} // call Test as constructor, Const test = new test () test.say() // helloCopy the code

As soon as the function is created, we get a prototype object that we can extend. In this case we add the say method to the prototype object. We then call the function with the new operator, this time as a constructor, creating a new object and setting it as the context of the function (accessible through the this keyword). The new operator returns a reference to the new object. Then we see that the generated test (a reference to the newly created object) has the SAY method.

  • Each function has a prototype object.
  • Each function prototype has a constructor property that points to the function itself

As you can see from the figure above, each function we create has a new prototype object. The original prototype object had only one property, the constructor property. This property points to the function itself.

When we call a function as a constructor, the stereotype of the newly constructed object is set as a reference to the stereotype of the constructor. In the example above we added the say method to test. prototype. When Test is created, the prototype of object Test is set to Test’s prototype. Therefore, the say method is called through test, delegating the lookup to test’s prototype object. Note that all objects created by Test have access to the say method. Now we see a way to reuse code.

Note: The say method is a stereotype property of Test, not an instance property of Test

7.2.1 Instance Properties

When a function is called as a constructor through the operator new, its context is defined as the new object instance, initialized by the constructor’s arguments. In the following example, we examine the properties of the instance created using this method.

Function Test() {this.flag = false this.say = function () {console.log(! Test.prototype.say = function () {console.log(this.flag)} // Which const will be used first in the Test Test = new test () test.say() // true uses methods on instancesCopy the code

The test found that the method was called on the instance. When the SAY attribute is accessed through test, the attribute, which is available in the instance, will not look up the stereotype.

function Test() {

}

Test.prototype.flag = false

const test = new Test()
const test1 = new Test()

Test.prototype.flag = true

console.log(test.flag, test1.flag) // true true
Copy the code

Each instance gets a separate version of the property created in the constructor, but they all have access to the same stereotype property

7.2.3 Implement object types through constructors

Each function’s prototype object has a constructor property that points to the function itself. By using the Contructor property, we can access the function used to create the object. This feature can be used for type validation.

Check the instance’s type and its constructor

function Test() {}

const test = new Test()

console.log(typeof test) // 'object'

console.log(test instanceof Test) // true
Copy the code

Typeof is just a way of knowing that test is an object, and Instanceof checking the typeof test provides more information — test is constructed from test.

In addition, we can use the Constructor property, which all instance objects have access to. The Contructor property is a reference to the function that created the instance object, and we can use the Constructor property to verify the original type of the instance.

7.3 Inheritance

Inheritance is a form in which a new object reuses the properties of an existing object. This helps avoid duplicate code and duplicate data. In Js, inheritance works a little differently from other popular object-oriented languages.

Use stereotypes to implement inheritance

Function Person(){} person.prototype. dance = function (){} function Test(){} Prototype = new Person() const Test = new Test() test.dance() console.log(Test instanceof Person) // trueCopy the code

When we define a Person function, we also create a prototype of Person, which references the function itself through its constructor property. Normally, we can extend the Person prototype with additional properties. In this case, we extend the dance method on the Person prototype, so that each Person instance object also has the dance method. We also define a Test function. The function’s prototype also has a constructor property pointing to the function itself. Next, to implement inheritance, assign the prototype of Test to an instance of Person. Now, whenever a new Test instance object is created, the prototype of the newly created Test instance object is set to the object to which the prototype attribute of Test points.

When we try to access the dance method through the Test instance object, the Js runtime will first look up the Test object itself. Since test itself does not have a dance method, the proto of the test object is then searched as an instance object of Person. The Person instance object also doesn’t have a dance method, so I looked for the prototype of the Person instance object and found the dance method. This is how inheritance is implemented in Js.

7.3.1 Problem with overriding the constructor attribute

We’ll see that using the example above for prototype inheritance, we’ve lost Test and its association with the original Test prototype by taking the Person instance object as the prototype for the Test constructor.

console.log(test.constructor === Test) // false
Copy the code

We can fix this, but before we can fix it, we have to take a look at the functionality provided by Js to configure properties of objects. In Js, objects are described by property description. We can configure keywords.

  • The property can be modified or deleted if the 64x is set to true. If it is set to false, changes are not allowed.
  • Enumerable, if set to true, can appear in for-in loops for object properties
  • Value is undefined by default
  • Writable if set to true, the attribute value can be modified through an assignment statement
  • Get defines the getter function. It is called when accessing properties. It cannot be used together with valuew and writable
  • Set defines setter functions, which are called when assigning values to properties, and cannot be used in conjunction with valuew and writable

Create object attributes with simple assignment statements, such as:

test.name = 'wave'
Copy the code

The assignment statement creates properties that can be modified or deleted, traversable, and writable. The name property of test is set to wave, and the get and set functions are both undefined.

If we want to adjust the configuration information of a property, we can use the built-in Object.defineProperty method, passing in three parameters: the Object of the property, the property name, and the property description Object.

Configuration properties

Let test = {} test.name = 'wave' test.age = 18 'isFlag', { configurable: false, enumerable: false, value: true, writable: true }) for(let prop in test) { console.log(prop) // name // age } console.log(test.isFlag) // trueCopy the code

Although we can access the isFlag property normally, we cannot traverse it in a for-in loop. Because Enumerable is false, you cannot iterate over the property in a for-in loop. To understand why, let’s go back to the original question.

Finally, the constructor attribute is overridden

To implement Test inheriting from Person, we lose the Test prototype from Constructor when we set the Test prototype to an instance object of Person. We do not want to lose the construcotor property, which can be used to determine the constructor used to create an object instance.

We can solve this problem by using the knowledge we’ve just acquired. Add a new constructor attribute to the test. prototype Object using the Object.defineProperty method.

Resolve the constructor property problem

function Person(){} Person.prototype.say = function () { console.log('hello') } function Test(){} Test.prototype = new Person() Object.defineProperty(Test.prototype, 'constructor', { enumerable: false, value: Test }) const test = new Test() console.log(test instanceof Test) // true for(let prop in Test.prototype) { Console. log(prop) // only say}Copy the code

This re-establishes the connection between test and the constructor test. So you can be sure that the test instance was created through the Test constructor. In addition, if you iterate over the test. prototype object, you can be sure that the constructor property is not accessed.

7.3.2 Instanceof operator

In most programming languages, the most straightforward way to detect whether an object is a class is to use the instanceof operator. For example, in Java, instanceof is used to check whether the class on the left is the same subclass as the class on the right. While the instanceof operator in Js is similar to that in Java, there are some differences. In Js, the instanceof operator is used in the prototype chain. For example, see the following expression

test instanceof Test
Copy the code

The instanceof operator is used to check whether the prototype of the Test function exists in the prototype chain of the Test instance. Let’s go back to Person and Test for a more concrete example.

Explore the instanceof operator

function Person(){}
function Test(){}

Test.prototype = new Person

const test = new Test()

console.log(test instanceof Person) // true
console.log(test instanceof Test) // true
Copy the code

The prototype chain of the test instance consists of the new Person() object and the prototype of Person, so (test instanceof Person) is certain to be true. When the (test instanceof test) expression is executed, the Js engine checks whether the new Person() object, the prototype of the test function, exists on the prototype chain of the test instance. The new Person() object is a prototype of the Test instance, so the expression is also executed true.

Note: instanceof checks to see if the prototype of the function to the right of the operator exists on the prototype chain of the object to the left of the operator.

Warning for the instanceof operator

Changes to the constructor prototype also change the result returned by instanceof

function Test(){}
const test = new Test()

console.log(test instanceof Test) // false

Test.prototype = {}

console.log(test instanceof Test) // true
Copy the code

In the above code, we reassign the Test prototype, and then execute the Test instanceof Test. We find that the result has changed. It turns out that the instance operator does not test whether an object was created by a function constructor, but rather whether the function prototype on the right exists in the prototype chain of the object on the left of the operator.

7.4 Using THE Js class on ES6

7.4.1 Using the keyword class

ES6 introduces the new class keyword, which provides a more elegant way to create objects and implement inheritance, while the underlying implementation remains prototype-based.

Create classes in ES6

Class Test {// define a constructor, when the class is called with the new keyword, Constructor (name) {this.name = name} // define a method accessible to all Test instances say() {console.log('hello')}} const Test = new Test('wave') console.log(test.name) // wave test.say() // helloCopy the code

As shown in the above code, we can create the Test class by using the ES6 keyword class, create a constructor in the class, and call that constructor when we use the class to create instance objects. Within the constructor body, the newly created instance can be accessed through this, and it is easy to add attributes, such as the name attribute. Within a class, you can also define methods that are accessible to all instance objects.

Class is grammatical sugar

Although ES6 introduces the keyword class, the underlying implementation is still based on prototypes. Class is just syntactic sugar, which makes mimicking class code in JS much simpler.

The above code can be converted to ES5 code as follows:

function Test(name) {
	this.name = name
}

Test.prototype.say = function () {
	console.log('hello')
}
Copy the code

You can see that there is nothing special about Es6’s classes, and while they look elegant, they use the same concepts.

A static method

The previous example showed how to define object methods (prototype methods) that are accessible to all instance objects. In addition to object methods, class-level static methods are generally used in the classical object-oriented language Java.

Static methods in ES6

class Test { constructor(name, Level) {this.name = name this.level = level} say() {console.log('hello')} // use the keyword static to create static methods compara(pram1, pram2) { return pram1.level - pram2.level } } const test1 = new Test('wave1', 6) const test2 = new Test('wave2', Console. log(test1.compara) // undefined // Test can access console.log(Test.compara(test1, test2)) // 5Copy the code

Let’s take a look at how static methods were implemented in previous versions of ES6. We just need to remember to implement classes through functions. Because static methods are class-level methods, you can use first-type objects to add methods to constructors

function Test() {}
Test.compara = function() {}
Copy the code

7.4.2 Implementing Inheritance

To be honest, inheritance was a pain to implement in previous versions of ES6, so let’s take a look at the previous example

function Person(){}
Person.prototype.say = function () {
  console.log('hello')
}

function Test(){}
Test.prototype = new Person()

Object.defineProperty(Test.prototype, 'constructor', {
  enumerable: false,
  value: Test
})
Copy the code

Note: Methods accessible to all instances must be added directly to the constructor prototype, such as the dance method on the Person constructor. In order to implement inheritance, we must set the prototype derived from the instance object as a base class. In this case, we assign a new Person instance object to the Test prototype. Unfortunately, this messes up the constructor property, so it needs to be set manually via the Object.defineProperty method. To implement a relatively simple and generic inheritance feature, we need to keep this set of details in mind. But in ES6, the whole process has been greatly simplified.

Implement inheritance in ES6

Class Person {constructor(name) {this.name = name} say() {console.log('hello')}} class Person {constructor(name) {this.name = name} say() {console.log('hello')} extends Person { constructor(name, Level = level}} const test = new test ('wave', 999) console.log(test.constructor === Test) // true console.log(test instanceof Test) // true console.log(test.name, test.level) // wave 999 test.say() // 'hello'Copy the code

7.5 summary

  • A Js object is a collection of property names and property values
  • Js uses prototypes
  • Each object has a reference to the stereotype, and when searching for a specified property, if the property does not exist on the object itself, the search can be proxy to the stereotype. The prototype of an object can also have a prototype, and so on, forming a prototype chain
  • You can define the prototype of an Object using the Object.setPrototypeOf method
  • Stereotypes are closely related to constructors. Each function has a stereotype property, and the prototype of the object that the function creates is the prototype that points to the function
  • A function prototype object starts with a single property, constructor, which points to the function itself. All objects created by the function have access to this property, and the constructor property can also be used to determine whether it was created by the specified function.
  • In Js, almost everything changes at runtime, including object prototypes and function prototypes.
  • The introduction of the class keyword in ES6 makes it easier to implement mock classes. At the bottom, it is still implemented using prototypes.
  • Using extends makes inheritance more elegant

Access to control objects

This chapter includes the following:

  • Use getters and setters to control access to objects’ properties
  • Control access to objects through proxies
  • Use proxies to solve cross-access problems

8.1 Use getters and setters to control property access

8.1.1 Defining getters and setters

In Js(ES5), you can define getters and setters in two ways

  • Defined by object literals, or in the class of ES
  • By using the built-in Object.defineProperty method

Define getters and setters in object literals

const test = { arr: [1, 2, 3, 4], // Define the getter method for firstArr, return the first value of the arR list, Get firstArr() {console.log('getting firstArr') return this.arr[0]}, // define setter methods for firstArr, set the first value in the arR list, Set firstArr(value) {console.log('setting firstArr', value) this.arr[0] = value } } console.log(test.firstArr) test.firstArr = 5 console.log(test.firstArr)Copy the code

All the printed results are as follows:

  • Define the syntax for getters and setters, adding the keyword set or get before the property name
  • If a property has getter and setter methods, the getter method is implicitly called when accessing the property and the setter method is implicitly called when assigning a value to the property

Use getters and setters in classes in ES6

class Test {
  constructor() {
    this.arr = [1, 2, 3, 4]
  }

  get firstArr() {
    console.log('getting firstArr')
    return this.arr[0]
  }

  set firstArr(value) {
    console.log('setting firstArr', value)
    this.arr[0] = value
  }
}

const test = new Test()
console.log(test.firstArr)
test.firstArr = 5
console.log(test.firstArr)
Copy the code

The output is similar to the figure above.

Note: It is not necessary to define both getters and setters for specified properties. For example, we usually only provide getters. If we need to write property values in some cases, the behavior depends on whether the code is in strict or non-strict mode. If assigning to getter-only properties doesn’t work in non-strict mode, the Js engine will silently ignore our request. On the other hand, in strict mode, the Js engine will throw an exception indicating that we are assigning a value to a property that has only a getter and no setter.

Although it is easy to specify getters and setters via ES6 and object literals, you may have noticed some problems. Traditionally, getter and setter methods have been used to control access to private object properties, but unfortunately Js has no private object properties. We can simulate private object properties through closures by defining variables and specifying that the object contains them. But because object literals and classes, getters, and setter methods are not defined in the same scope, variables that are expected to be private object properties are not available. Fortunately, we can do this with the Object.defineProperty method.

Define getters and setters with Object.defineProperty

Function Test() {// Define a private variable that will be accessed through a closure let _skillLevel = 0 object.defineProperty (this, 'skillLevel', {get: () => { console.log('getter') return _skillLevel }, set: (val) => { console.log('setter') _skillLevel = val } }) } const test = new Test() console.log(typeof test._skillLevel) // 'undefined' console.log(test.skillLevel) test.skillLevel = 1 console.log(test.skillLevel)Copy the code

  • Regardless of how they are defined, getters and setters allow us to define object properties as standard object properties, but the getter and setter methods are called immediately when a property is accessed or assigned a value to the property. This is a very useful feature that allows us to perform logging, validate property values, and even notify other parts of the code when changes occur. (You can use getters and setters to validate property values and define computed properties)

8.2 Controlling access by proxy

Proxies allow us to control access to another object through proxies. The proxy allows you to define custom behavior that can be performed when objects interact, such as reading or setting property values, or calling methods. Proxies can be thought of as generic setters and getters, with the distinction that each one controls only a single object property, whereas proxies can be used for generic handling of object interactions, including methods that invoke objects.

Previous setters and getters for logging, data validation, calculating properties, and so on can all be handled using agents. The proxy is more powerful. Using proxies, we can easily add analysis and new metrics to our code. Automatically populate object properties to avoid nasty NULL exceptions. Wrapping host objects such as DOM is used to reduce cross-browser incompatibilities.

The Proxy is created through the Proxy constructor

// test is the target object const test = {name: Const proxyTest = new Proxy(test, {get:) const proxyTest = new Proxy(test, {get:); (target, key) => { console.log('getting', key, target) return key in target ? target[key] : 'none' }, set: (target, key, value) => { console.log('setting', Key) target[key] = value}}) // Access the name property console.log(test.name) console.log(proxytest.name) // from the target object and the proxy object respectively Log (test.nickname) // The nickname property does not exist. Console. log(proxytest.nickname) // After adding the nickname property to the proxy object, Proxytest.nickname = 'wang' console.log(proxytest.nickname) console.log(test.nickname)Copy the code

Printout of the above code:

The main points of using Proxy objects: The Proxy object is created through the Proxy constructor, and the Proxy object performs the specified operation when accessing the target object. In the example above, we used get and set. There are many other built-in methods for defining the behavior of various objects, as detailed in mng.bz/ba55 for example:

  • Apply is activated when a function is called and Constructor is activated when the new operator is used
  • Get and set are activated when properties are read/written
  • Enumerate is enabled when the for-in statement is executed
  • GetPrototypeOf and setPrototypeOf are activated when getting and setting property values

8.2.2 Using proxies to detect performance

In addition to logging property access, the proxy can also evaluate the performance of function calls without modifying the function code. For example, we want to evaluate the performance of a function that calculates whether a number is prime.

Use proxies to evaluate performance

IsPrime = new Proxy(isPrime, {// define the apply method. When the Proxy object is called as a function, the apply method will be triggered: (target, thisArg, args) => {// Start a timer console.time() // call the target function console.log(target, thisArg, args) const result = target.apply(thisArg, args) console.timeEnd() return result } }) console.log(isPrime) isPrime(11111139038030)Copy the code

Use the isPrime function as the target object for the proxy. Also, add the apply method, which is called when the isPrime function is called. We assign the newly created proxy object to the isPrime identifier. In this way, we can call the Apply method to evaluate the isPrime function without modifying the internal code of the isPrime function.

8.2.3 Using a proxy to automatically populate properties

In addition to simplifying logging, proxies can also be used to automatically populate properties.

For example, suppose you want to abstract a computer’s folder structure model, a folder object that can have properties or be folders. Now let’s say you need a long path of the file model, such as: rooFolder. FirstDir. SecondDir. The file = ‘test. TXT’

To create this long-path file model, you might design your code along the following lines:

const rootFoller = new Folder()
rootFolder.firstDir = new Folder()
rootFolder.firstDir.secondDir = new Folder()
rootFolder.firstDir.secondDir.file = 'test.txt'
Copy the code

This may seem like a necessary chore, and we may need to automatically populate the properties.

Use the proxy to automatically populate the properties

return new Proxy({}, { get(target, prop) { console.log(prop, target) if(! (prop in target)) { target[prop] = new Folder() } return target[prop] } }) } const rootFolder = new Folder() rootFolder.firstDir.secondDir.file = 'test.txt'Copy the code

Normally, this code would definitely throw an exception, but we used a proxy, so the proxy method is activated every time the property is accessed. If the accessed property does not exist, a new folder is created and assigned to the property.

8.3 summary

  • We can monitor objects using getters, setters, and proxies

  • By using accessor methods (getters and setters), we can control access to object properties. You can define access properties through the built-in Object.defineProperty method, or use get and set syntax or ES6 classes in Object literals. The get method is implicitly called when an object property is read and the set method is implicitly called when an object property is written. The getter method is used to define computed properties that are evaluated each time an object property is read. Similarly, setter methods can be used to implement data validation and logging.

  • Proxies, introduced in JSes6, can be used to control objects.

  • A proxy can customize the behavior of objects when they interact (for example, when properties are read or methods are invoked). All interactions must go through a proxy, and the proxy method is invoked when the specified behavior interacts.

  • Using proxies, you can elegantly implement the following:

Logging, performance testing, data validation, automatic population of object properties, array negative indexes

9. Processing sets

9.1 an array

Arrays are one of the most common data types. With arrays, you can work with collections of data. If your programming background is in strongly typed languages such as C, you may think of arrays as contiguously large chunks of memory that store the same data type, each of which is of a fixed size and has an associated index that makes it easy to access each item. Let’s take a look at arrays in Js

9.1.1 Creating an Array

There are two basic ways to create arrays:

  • Use the built-in Array constructor
  • Using array literals []

Note: Using array literals to create arrays is preferable to constructors. The main reason is simple: [] and new Array(). Additionally, due to the highly dynamic nature of JavaScript, there is no way to prevent modifications to the built-in Array constructor, which means that new Array() doesn’t have to create an Array. Therefore, it is recommended to stick with array literals.

Regardless of how an array is created, each array has a length attribute, which indicates the length of the array. Access an array element using an index. The first element has an index of 0. If you attempt to access an index outside the array length range, you do not throw an exception as in other languages, but return undefined. The **Js array is an object. If an object does not exist, undefined is returned. Accessing an array index that does not exist also returns Undefined.

On the other hand, if you write elements outside the array boundary, the array will expand to accommodate the new situation, as shown below:

As shown in the figure above, we actually create an empty element in the array. The element before index 5 is undefined. The length attribute is now 6. Unlike most other languages, Js also has a special feature on the length attribute: you can manually modify the value of the length attribute. If you change the length value to a number larger than the original value, the array will be expanded. The elements of the new expansion are undefined. If you change the length value to a number smaller than the original value, the data will be trimmed.

Let’s take a look at some of the most common array operations


These common methods, we have basically used and learned, so I will not introduce in detail, just sort out some of the details that need to pay attention to.

9.1.2 Adding and deleting elements at both ends of an Array

Performance considerations: POP and push vs. Shift and unshift Pop and push methods affect only the last element of the array: POP removes the last element, and push adds an element to the end of the array. The shift and unshift methods modify the first element, and the index of each subsequent element needs to be adjusted. Pop and push methods are therefore much faster than shift and unshift, which are not recommended except in special cases

Return value: Their return value is the element that was added or removed

9.1.3 Adding and removing elements anywhere in an array

The delete operator can delete elements, but this method of deleting an element simply creates an empty element in the array, which should be deleted and replaced using the splice method

Find: uses find to find elements in an array: returns the element on which the first callback returns true

IndexOf: finds the indexOf a specific element, passing the target element as a parameter (lastIndexOf finds the indexOf the last occurrence of the target element)

FindexIndex returns the element that the first callback returns true. Similar to find, the only difference is that find returns the element itself, whereas findIndex returns the index of the element.

The sort order

Js has the sort method, which is used as follows

array.sort((a, b) => a - b)
Copy the code

We need to provide a callback that tells the sorting algorithm the relationship between two adjacent array elements. Possible results are as follows:

  • If the return value of the callback is less than 0, element A should come before element B
  • If the return value of the callback function is 0, elements A and B should appear in the same place
  • If the return value of the callback is less than 0, element A should appear after element B

Aggregate array elements using reduce:

const arr = [1, 2, 3, 4]

const sum = arr.reduce((aggregated, number) => {
  return aggregated + number
}, 0)

console.log(sum) // 10
Copy the code

The reduce function passes in a total value and the current element to each callback function, eventually returning a value. The second parameter is the initial value passed in.

Reuse the built-in array functions

Const elems = {// To simulate the length of the array, store the number of elements in the collective length: 0, // implement the add method to add elements to the collection. Directly using the method of the prototype of the Array to add (elem) {Array. Prototype. Push. Call (this, elem)}, Gather (ID) {this.add(document.getelementById (ID))} Look for the element in the set method find (the callback) {return Array. The prototype. The find. Call (this, callback) } } elems.gather('first') console.log(elems.length) // nodeType : 1 is the element node, Log (elems[0].nodeType) elems.Gather ('second') console.log(elems.find(item => item.id === 'second'))Copy the code

Output result: In the example above, we create objects and simulate the behavior of some arrays. The length attribute is first defined to store the number of elements, similar to an array. Then define the Add method that adds elements to the end. Use the built-in array method find to implement the find method of a custom object, used to find any element in the custom object.


Let’s continue with the two new collection types introduced in ES6: Map and Set

9.2 the Map

Suppose we build a website that needs to meet the needs of a global audience. It is necessary to create languages corresponding to different countries, such as Korean and English. Such collections, which Map keys to specified values, have different names in different programming languages and are often called dictionaries or maps.

But how do you manage this positioning effectively in Js? A traditional approach is to create the following dictionary using the property that objects are attribute names and attribute values:

Const dictionary = {' useful ': {' ninja' : 'ninja'}, "ko" : {' ninja ':' 닌 자 '}}Copy the code

At first glance, this seems to work out perfectly, which is fine for this example, but in general it’s not reliable.

9.2.1 Do not treat objects as maps

Suppose you need access to the constructor property of a word on a web site.

Const dictionary = {'zh': {'ninja': '닌 자'}} console.log(dictionary.zh['constructor'])Copy the code

Attempt to access the constructor property, which is a word not defined in the dictionary. Should have returned undefined. But that’s not how it turned out. The result is as follows:

Because you can access object properties that are not explicitly defined through stereotypes, objects are not optimal maps. Every object has a stereotype, and although you define a new empty object as a map, you can still access the properties of the stereotype object. One of the properties of the prototype object is constructor, which leads to confusion.

Also, the key of the object must be a string. If you want to map to another type, it is silently converted to a string without any hint. Take a look at the example below, assuming you need to track HTML node information.

Const firstElement = document.getelementById ('first') const secondElement = document.getelementById ('second') Const map = {} // store the firstElement information and verify that it is correct map[firstElement] = {data: 'firstElement'} console.log(map[firstElement].data === 'firstElement') // Store the second element information, Map [secondElement] = {data: 'secondElement'} console.log(map[secondElement]. Data === 'secondElement') Log (map[firstElement].data === 'firstElement') // Print map console.log(map)Copy the code

Output result:

The reason for this is that the object’s key must be a string, which means that when you try to use a non-string type such as an HTML element as a key, its value is silently converted to a string type by the toString method. The HTML element (input) is converted to a string value of [Object HTMLInputElement]. The data information of the first element is stored in the [Object HTMLInputElement] attribute. When storing the second element, the value of the first element is overwritten.

** You can’t usually use objects as maps for two reasons: ** Stereotypes inherit attributes and keys only support strings. Because of this limitation, the ECMAScript committee decided to define a new type: Map

9.2.2 create map

Use the new built-in constructor Map

Const test1 = {name: 'wave1'} const test2 = {name: wave1} const test2 = {name: 'wave2'} const test3 = {name: 'wave3'} const test3 = {name: 'wave3'} const test3 = {name: 'wave3'} const test3 = {name: 'wave3'} 18 }) testMap.set(test2, { age: Log (testmap.get (test1).age === 18) console.log(testmap.get (test2).age === 19) // Use the Map get method to obtain the object console.log(testmap.get (test1).age === 18) // Log (testmap.get (test3) === undefined) console.log(testmap. size === 2) // Use the HAS method to verify whether the specified key exists in the map Console. log(testmap. has(test1)) // Use the delete method to delete the specified key testmap.delete (test1) console.log(testmap.size) // Use the clear method to completely clear the map  testMap.clear() console.log(testMap.size)Copy the code

The output from the above code is:

We create the map using the set method and get method. In addition to these two methods, map also has the size attribute, has, delete, and clear methods. The size attribute tells us how many maps have been created. A map is a collection of key-value pairs. Keys can be any type of value, or even objects.

9.2.2 traverse map

So far, we’ve seen some of the advantages of maps: you can be sure that only what you put into a map exists, you can use any type of data as a key, and so on.

Because a map is a collection, you can use a forOF loop to iterate over the map. You can also ensure that the order of traversal is the same as the order of insertion. (A for-of or for-in loop on an object is not guaranteed). This is the result of different browser Js engines following different specifications, which extract all attributes of the key whose parseFloat value is a non-negative integer, then sort the attributes in numerical order, first iterating through them, and then iterating through the remaining attributes in the order defined by the object. Let’s take a look at an example:

var obj = { name: 'abc', '3': 'ccc', age: 23, school: 'sdfds', class: 'dfd', hobby: 'dsfd' }

// console.log(Object.keys(obj))

for(let key of Object.keys(obj)) {
  console.log(key)
}
Copy the code

The output is: The key ‘3’ becomes the first digit.

Next we iterate over the map

const directory = new Map() directory.set('name', 'wave') directory.set('3', 'age') directory.set('address', 'shenzhen') // each element has two values, Key and value for(let item of directory) {console.log('key:' + item[0], 'value:' + item[1])} console.log(' split — — — — — — — — — — — —') // You can iterate over all keys for(let key of) using the built-in keys method Directory. Keys ()){console.log(key)} console.log(' split — — — — — — — — — — — —') // You can use values to iterate over all values for(let value of)  directory.values()) { console.log(value) }Copy the code

The following figure shows the output:

Now that we know about maps, let’s look at another new Set type: Set, where elements are unique.

9.3 the Set

In many practical problems, we have to deal with sets in which each element is unique and can occur only once, called sets. Prior to ES6, this kind of collection could only be implemented through emulation.

Simulate sets through objects

Set.prototype.has = function(item) {this.data = {} this.length = 0} set.prototype. has = function(item) { return typeof this.data[item] ! Set.prototype.add = function(item, data) {console.log(! this.has(item)) // if(! {this.has(item)) {this.data[item] = data this.length.length ++ //}} If (this.has(item)) {delete this.data[item] this.length--}} const test = new Set() test.add('name', 'wave') test.add('name', 'wave') console.log(test)Copy the code

The output is: This is just simulation, and you have the same problem, you can’t really store objects, you can only store strings or numbers, and you still have the risk of accessing prototype objects. Because of these issues, the ECMAScript committee decided to introduce an entirely new collection type: Set.

9.3.1 create the Set

The Set is created using the constructor: Set

Create a Set

const test = new Set(['name', 'age', 'weight', 'name']) console.log(test.has('name')) // Discard duplicate entries console.log(test.size) console.log(test.has('sex')) test.add('sex') Log (test.has('sex')) console.log(test.size) // Adding existing elements to the collection has no effect test.add('sex') console.log(test.size)Copy the code

Create a Set using the built-in constructor. If no arguments are passed, an empty Set is created

The members of a Set are unique, and the most important function of a Set is to avoid storing multiple identical objects. In this case, you tried to add ‘sex’ twice but only succeeded once. Has verifies the presence of the element in a Set. If you want to know how many elements a Set has, you can use the size attribute. Like maps and arrays, sets are collections, so you can iterate through them using a for-of loop. The traversal order is the same as the insert order.

Next, let’s look at some common operations on Set: union, intersection, and difference.

9.3.2 and set

Const arr1 = [1, 2, 3] const arr2 = [2, 3, 4] const newSet = newSet ([...arr1, ...arr2]) console.log(newSet.size) // 3Copy the code

9.3.3 intersection

const set1 = new Set([1, 2, 3]) const set2 = new Set([2, 3, Const newSet = newSet ([...set1].filter(item => set2.has(item)))Copy the code

9.3.4 difference set

const set1 = new Set([1, 2, 3]) const set2 = new Set([2, 3, Const newSet = newSet ([...set1].filter(item =>! set2.has(item)) )Copy the code

9.4 summary

  • Arrays are special objects that have the length attribute and are prototype array.prototype
  • Arrays can be created using Array literals ([]) or Array constructors.
  • You can display the defined object methods on a custom object and reuse the methods of an array using the Call or apply methods.
  • Maps and dictionaries are objects that contain mappings between keys and values.
  • Objects in javaScript are bad maps that can only use string types as keys and run the risk of accessing stereotype attributes. Therefore, use the built-in Map collection.
  • You can use for… The of loop through the Map collection
  • The values of Set members are unique

Regular expressions

Regular expressions are a necessity in modern development. While many developers can get things done without regular expressions, many problems can’t be solved elegantly without regular expressions. In this article, only a brief introduction will be given.

Regular expressions are the process of breaking up strings and looking up information. In the mainstream Js libraries, you can see developers making extensive use of regular expressions to solve various tasks:

  • Manipulate strings in HTML nodes
  • Use CSS selector expressions to locate partial selectors
  • Determines whether an element has the specified class name
  • Input validation
  • Other characters

10.1 Why are Regular expressions Needed

If the user enters a string in a form on a web page that needs to follow a zip code, 99999-9999. We need to validate the input.

We can create a function that verifies that the specified string conforms to the gSA format. But this is tedious. If we use re:

   function isRight(str) {
    return /^\d{5}-\d{4}$/.test(str)
  }
Copy the code

10.2.1 Description of regular expressions

We simply think of regular expressions as forms that use patterns to match literal strings. The expression itself has terms and operators that define the pattern.

In Js, there are two ways to create regular expressions, just like any other object type.

  • Use regular expression literals
const pattern = /test/
Copy the code
  • By creating an instance of the RegExp object
const pattern = new RegExp('test')
Copy the code

** Note: ** Literal syntax is preferred when regular expressions are explicit in the development environment, and constructor expressions are used when they need to be created dynamically at runtime

Literal syntax is preferred, in part, because backslashes play an important role in regular expressions. But backslashes are also used to escape characters. Therefore, a double backslash is needed to identify \ for the backslash itself.

In addition to the expression itself, there are five modifiers you can use:

  • I is case-insensitive. For example, /test/ I matches both test and test
  • G searches for all matches and does not stop when it finds the first match, but continues to search for the next match
  • M allows multi-line matching, which is useful for retrieving the value of the TestArea element.
  • Y starts to stick and match. Regular expression execution adhesion
  • U – Allows Unicod point escapes

Use: Add a modifier to the end of a literal (such as /test/ig), or pass it as a second argument to the RegExp constructor (new RegExp(‘test’, ‘ig’))

10.2.2 Terms and operators

Exact matching Except for non-special characters and operators, characters must appear exactly in the expression. For example, the four characters in the re /test/ must appear entirely in the matched string, one character after the other.

Matching character set [ABC] Matches any character in A, B, and C. If we want to match any character other than a, b, or c, we can add a sharp corner after the opening parenthesis ^ : [^ ABC]

There is a more important operation for character sets: scoping. For example, matching lowercase letters between a and m can be expressed as [abcdefghijklm], but it is more concise to write: [a-m]

Escape note that not all characters and character literals are equivalent. We can see that the characters [,], -, ^ represent something other than themselves. So, how do we match the character [itself? In regular expressions, the backslash escapes the character following it to match the meaning of the character itself. So, [matches the character [instead of the parentheses that represent the group of characters.

Start and stop notation We often need to make sure that we match either the beginning of the string, or the end of the string. The sharp corner is used to match the beginning of the string such as /^test/ matches test at the beginning of the string. Similarly, the dollar sign indicates the end of the string: /test indicates the end of the string: /test/, with both ^ and $to match the entire string.

repeated

  • Specifies the optional character (which can occur 0 or 1 times) followed by? , for example, /t? Est/can match both test and EST
  • To specify that the string must occur one or more times, use +
  • Zero or one or more times, use *
  • Specify the number of times {} is used, such as /a{4}/ to match four consecutive characters a
  • Specify a range of cycles, separated by commas, e.g. /a{4, 10}/ matches 4-10 consecutive characters a
  • Specify an open interval, omitting the second value and leaving the comma. For example, /a{4,}/ matches four or more consecutive characters a

Predefined character set

There are many, here just sort out some commonly used!

Predefined metacharacter The matching character set
\d Matches any decimal number, equivalent to [0-9]
\D Matches any character except a decimal number, equivalent to [^0-9]
\w Matches any letters, digits, and underscores, equivalent to [A-zA-z0-9_]
\W Matches characters other than letters, digits, and underscores, equivalent to [^ A-za-z0-9_]
\s Matches any whitespace character
\S Matches any character except whitespace

Grouping uses (). If you use the operator for a set of terms, you can group them using parentheses, for example, /(ab)+/ matches one or more consecutive ab.

OR the operator (OR) using the vertical bar (|) OR. For example, a | b/can match a or b.

Backreference Backreferences a capture defined in a referenceable re. For the moment, we can think of the capture as a matching string, that is, the string we matched earlier. A backreference to something captured in a group uses a backslash followed by a number, starting with 1. The first group captures \1, the second captures \2, and so on.

For example ^([DTN])a\1, matches any string starting with d or t or n, with consecutive a, and matches the contents captured in the first grouping consecutively. The latter point is important, this is different from /[DTN] a[DTN]/. The a character must be followed by the previous matching letter. Thus, the character \1 indicates matching until equality is known.

Backreferences are useful when matching markup elements of XML type. Look at the following re: /<\w+>(.+)
/ this can match simple elements like whatever. You might not be able to do this without a backreference, because you cannot know in advance what the closing tag will be that matches the start tag.

In addition, we can call the string replace method to get the capture within the replacement string. Instead of using a backreference, we can capture ordinals with tags like 1,1,1,2,$3, etc

11. Modularity of code

Modules are units of code that are larger than objects and functions and can be used to categorize programs. When creating modules, we should strive for consistent abstraction and encapsulation. This helps you think about your application and avoid being distracted by trivial details when using modular functionality. In addition, using modules means that it is easier to reuse module functionality across different parts of an application, and even across applications, greatly increasing application development efficiency.

When we define a variable in the main thread code, the variable is automatically recognized as a global variable and can be scoped by other parts of the code. This may not be a problem for small programs, but as the application expands to reference third-party code, the potential for naming conflicts increases dramatically. Prior to ES6, Js did not provide advanced built-in features. This built-in space is used to encapsulate variables in modules, namespaces, or packages. To solve this problem, Js programmers have developed advanced modularity techniques that take advantage of existing features such as objects, instant-execution functions, and closures.

11.1 Modularize code in versions prior to ES6

Prior to JsES6, there were only two types of scope: global and function scope. There is no scope in between, no namespace or module to group functionality. In order to write modular code, Js developers have to be creative with the existing syntax features of Js.

  • Defines a module interface through which functions of the module can be invoked.
  • Hide the internal implementation of the module to avoid possible side effects and unnecessary changes to bugs

We start by creating modules using objects, closures, and instant-execution functions. We then move on to the most popular modular standards, AMD and CommonJs, which have slightly different fundamentals, and explain how to use them, as well as the pros and cons of each.

Module pattern: Extending modules with functions, implementing interfaces with objects Module interfaces typically contain a set of variables and functions. The easiest way to create an interface is to use a Js object. For example, uncounted page click on the module to create an interface.

  const ClickConutModule = function() {
    let numClicks = 0

    const  handleClick = () => {
      console.log(++numClicks)
    }

    return {
      conutClick: () => {
        document.addEventListener('click', handleClick)
      }
    }
  }()

  ClickConutModule.conutClick()

  console.log(ClickConutModule.handleClick) // undefined
  console.log(ClickConutModule.numClicks) // undefined
Copy the code

In this example, the module is implemented using immediate execution functions. Inside the immediate execution function defines the implementation details inside the module, a local variant numClicks, and a local function handleClick, which can only be accessed inside the module. Expose the module’s public interface through the returned object. Implementations inside a module (private variables and functions) are kept active through closures created by the public interface.

This way of creating modules in Js using immediately executing functions, objects, and closures is called the module pattern. Once we have the ability to define modules, we can split different modules into multiple files, or define more functionality on existing modules without modifying the existing code.

Expand module

ClickConutModule.newMethod = () => { ... return ... } ()Copy the code

Modules that extend functions with standalone immediate execution cannot share module private variables, because each function creates a new scope separately. There are other problems with the modular model. When we start building modular applications, modules themselves often depend on the functionality of other modules. However, the modular pattern cannot implement these dependencies.

To solve this problem, two standards have emerged: AMD and CommonJS

11.1.2 Use AMD and CommonJs modular Js applications

AMD and CommonJS are two competing standards, and both can define modules. Apart from syntax and principles, the main difference is that AMD’s design philosophy is explicitly browser-based, while CommonJs is designed for a general-purpose Js environment. Not just the browser.

AMD

AMD(Asynchronous Module Definition) is an Asynchronous Module Definition. Asynchrony emphasizes that when loading modules and other modules on which modules depend, asynchrony loading is adopted to avoid module loading blocking the webpage rendering progress. AMD originated with the Dojo Toolkit, one of the popular JavaScript tools for building client-side Web applications. AMD can easily specify modules and dependencies. It also supports browsers. AMD’s most popular implementation is RequireJS.

AMD, as a specification, just defines its syntax API, not its implementation. The AMD specification is as simple as a single API, the define function.

Using AMD to define modules relies on JQuery

// Use define to specify modules and their dependencies, The module factory function creates the corresponding module define('MouseCounterMOdule', ['Jquery'], $ => { let num = 0 const handleClick = () => { console.log(++num) } return { countClick: () => { $(document).on("click", handleClick) } } })Copy the code

AMD provides a function called DEFINE, which takes the following parameters

  • ID of the newly created module. With this ID, you can reference the module in other parts of the system.
  • List of module ids on which the current module depends
  • Initializes a module’s factory function that takes a list of dependent modules as arguments

In the previous example, we used AMD’s define function to define the module with the ID MouseCounterMOdule. This module relies on JQuery. Because of its dependence on JQeury, AMD requests the JQuery module first, and if it needs to request it from the server, this process will take some time. This process is performed asynchronously to avoid blocking. After all dependent modules have been downloaded and parsed, the module’s factory function is called. And pass in the dependent modules. In this case, only one module is relied on so only one parameter, JQuery, is passed in. Inside the factory function is the process of creating a module similar to the standard module pattern: creating objects that expose the module’s common interface.

It can be seen that AMD has the following advantages:

  • Dependencies are handled automatically, regardless of the order in which modules are introduced.
  • Asynchronously load modules to avoid blocking
  • Multiple modules can be defined in the same file

CommonJS

ConmonJS uses file-based modules, so only one module can be defined per file. ConmonJS provides the variable Module, which has the property exports from which you can easily extend additional properties. Finally, module.exports is the public interface for this module. You can reference modules if you want to use them in other parts of your application. File synchronous loading, access to the module public interface. This is why CommonJS is more popular on servers where modules load relatively quickly and only need to read from the file system, whereas on the client side you have to download files from a remote server and synchronous loading usually means blocking.

Use CommonJS to define modules

const $ = require('JQuery')

let numClicks = 0
const handleClick = () => {
	console.log(++numClicks)
}

module.exports = {
	countClicks: () => {
    	$(document).on('click', handleClick)
    }
}
Copy the code

To reference the module in another file, write:

const MouseCounterModule = require('MouseCounterModule.js')
MouseCounterModule.countClicks()
Copy the code

Since ConmonJS requires a file to be a module, the code in the file is part of the module. Therefore, variables defined in a module are safely contained in the current module and will not be leaked to the global scope. Only objects or functions exposed through module.exports objects can be accessed outside the module.

ComonJS has two advantages:

  • Simple syntax. You only need to define the module.exports property and the rest of the module code is the same as standard JS. The way to refer to a module is also simple, just use the require function.
  • CommonJS is the default module format for Node.js, so we can use thousands of packages on NPM.

The biggest disadvantage of ConmonJS is that it does not explicitly support browsers. Browser-side JS does not support module variables and export attributes, so we have to package our code in a browser-supported format, either Browerify or RequireJS.

The ECMAScript Committee has recognized the need for a module syntax that supports all JS environments, so ES6 defines a new module standard that will finally address these issues.

11.2 ES6 module

ES6 modules combine the advantages of CommonJS and AMD as follows:

  • Similar to CommonJS, ES6 modules have relatively simple syntax and are file-based (each file is a module)
  • Similar to AMD, the ES6 module supports asynchronous module loading.

The main idea behind ES6 modules is that modules must be explicitly exported using identifiers in order to be accessed externally. To provide this functionality, ES6 introduces two keywords:

  • Export – Specifies the identifier from outside the module
  • Import-import module identifier

The import/export module syntax is relatively simple, but there are many subtle differences that we will explore step by step.

Export and import functions

Let’s start with a simple example

Export from the Ninja. Js module

const ninja = 'wave'

export const message = 'hello'

export function sayHi() {
	return message + ' ' + ninja
}
Copy the code

Export in the last line of the module

const ninja = 'wave'

const message = 'hello'

function sayHi() {
	return message + ' ' + ninja
}

export { message, sayHi }
Copy the code

This way of exporting module identifiers is similar to the module pattern in that the return object of the direct function represents the module’s public interface. In particular, like CommonJS, we extend the module.exports object through the public module interface. No matter how we export a module, if we need to import it in another module, we must use the keyword import.

Import the variable message and function sayHi from the module Ninja.js using the keyword import

import { message, sayHi } from 'ninja.js'
Copy the code

With modules, you can make your code safer by avoiding the abuse of global variables. Exported content that is not displayed can still be isolated by modules.

Import all identifiers in Ninja.js

import * as ninjaModule from 'Ninja.js'
Copy the code

We import all identifiers using the symbol * and specify the module alias using the AS keyword, after which the name must be defined using AS

The default is derived

Export Default class Ninja {constructor(name) {this.name = name}} export function export default class Ninja {constructor(name) {this.name = name}} export function comparaNinjas(ninja1, ninja2) { return ninja1.name === ninja2.name }Copy the code

Add the keyword default after the keyword export to specify the default export of the module. In this case, the module exports the class Ninja by default. Although the module’s default export is specified, additional identifiers can still be exported.

Import the content that the module exports by default

Import ImportNinja from 'ninja.js' import {comparaNinjas} from' ninja.js' import {comparaNinjas} from 'ninja.js'Copy the code

The default export does not need to use curly braces, and we can give the default export a custom name, not necessarily the export name. You can also import like this:

import ImportNinja, { comparaNinjas } from 'Ninja.js'
Copy the code

Use the comma operator in a single statement to separate the default and named exports imported from the Ninja.js file

We can also alias the export and import identifiers using the AS keyword to avoid naming conflicts.

11.3 summary

  • Small, well-organized code is far easier to understand and maintain than large code. One way to optimize program structure and organization is to break code up into small, loosely coupled pieces or modules.

  • Modules are units of code that are slightly larger than objects or functions and are used to organize programs into categories.

  • In general, modules reduce the cost of understanding, are easy to maintain, and can improve the reusability of code.

  • Prior to JavaScript ES6, there were no built-in modules and developers had to be creative with the existing features of the Js language to achieve modularity. One of the most popular ways to implement a module is through a closure that executes the function immediately:

  1. Use immediate functions to create closures that define module variables that are not accessible from an external scope.
  2. Use closures to keep module variables active.
  3. The most popular is the module pattern, which typically executes functions immediately and returns a new object as the module’s public interface
  • In addition to module mode, there are two popular module standards: AMD, which can be used on the browser side. CommonJS, which is more popular on JavaScript servers. AMD can automatically resolve dependencies, load modules asynchronously, and avoid blocking. CommonJS has a simple syntax, synchronously loads modules (and therefore is more popular on the server side), and you can get a lot of modules through NPM.

  • ES6 combines the features of AMD and CommonJS. ES6 modules are influenced by CommonJS, have simple syntax, and provide an asynchronous module loading mechanism similar to AMD.

  1. ES6 modules are based on files, and a file is a module
  2. Export is used to export identifiers and import is used to import identifiers

3. Modules can be exported by default, using an export to export the entire module. 4. Both export and import can be alias through keyword as.

12. DOM manipulation

Now there are a lot of JsDOM operation library source, we will understand the realization principle of DOM operation in the library, can not only cooperate with the class library to write more efficient code, but also can use these technologies flexibly in their own code. So we’ll start with this chapter and see how to extend existing pages by injecting HTML as needed.

12.1 Injecting HTML into the DOM

In the following scenario, we need to efficiently insert an HTML string anywhere in the document:

  • Insert arbitrary HTML into a web page and operate when inserting a client template
  • Pull and inject the HTML returned from the server

We’ll implement a clean set of DOM manipulations from scratch. The specific steps are as follows:

  • Converts any valid HTML string to a DOM structure
  • Inject DOM structures anywhere as efficiently as possible

12.1.1 Converting HTML Strings to DOM

HTML string to DOM structure is not special. In fact, it mostly uses a tool that everyone is familiar with: the innerHTML attribute.

The transformation steps are as follows:

  1. Make sure the HTML string is valid
  2. Wrap it in any closed TAB that conforms to the browser rules.
  3. Insert this string of HTML into a requirement DOM using innerHTML
  4. Extract the DOM node

Preprocess the HTML source string

The following code is a skeleton HTML

<option>Wave</option>
<option>Kuma</option>
<table/>
Copy the code

There are two problems with this code:

  1. Option elements cannot exist in isolation. If you follow good HTML semantics, they should be included in the SELECT element.
  2. Although markup languages usually allow self-closing tags with no child elements, such as<br/>, but only a few elements in HTML support it

We can do quick preprocessing of HTML strings, such as converting a

element to

.

Packaging HTML P307 here will not do a detailed record, I think this is not the current need to focus on learning content.

12.1.2 Inserting DOM Elements into a document

We have an array of elements that need to be inserted, possibly anywhere in the document, and we try to minimize the number of insertion steps. To do this, we can use DOM fragments for insertion. DOM fragments are part of the W3C DOM specification, which provides a container for storing temporary DOM nodes.

First look at the createDocumentFragment() method

  1. The createDocumentFragment() method is used to create a virtual node object, or document fragment node. It can contain various types of nodes and is empty when it is created.

  2. The DocumentFragment node is not part of the document tree and the inherited parentNode property is always null. One useful feature is that when a DocumentFragment node is requested to be inserted into the document tree, it is not the DocumentFragment itself that is inserted, but all of its descendants, i.e. the nodes in parentheses are inserted. This feature makes the DocumentFragment a placeholder for nodes that are inserted into the document at once. It also facilitates cutting, copying, and pasting of documents. In addition, when multiple DOM elements need to be added, if these elements are first added to the DocumentFragment and then the DocumentFragment is uniformly added to the page, the dom rendering times of the page will be reduced and the efficiency will be significantly improved.

  3. When a node in the original DOM tree is added to a DocumentFragment using the appendChid method, the original node is removed.

If you want to learn more about the usage of this method can go to the link below to see: blog.csdn.net/qiao1363342…

12.2 DOM Features and Attributes

When accessing the attribute values of element pairs, we have two choices: choose the traditional DOM methods getAttribute and setAttribute, or use the corresponding attribute on the DOM object.

For example, if an element is stored in variable e, we can get its ID as follows:

e.getAttribute('id')
e.id
Copy the code

Note: feature (LLDB attribute (‘id’)) and corresponding attribute value (e.id) do not share the same value, although related, but not always the same.

To access our custom attributes, which are not automatically represented by element attributes, we need to use the DOM method getAttribute().

    const myDiv = document.querySelector('div')

    myDiv.id = 1

    console.log(myDiv.getAttribute('id'))

    myDiv.setAttribute('myId', 666)

    console.log(myDiv.myId)
    console.log(myDiv.getAttribute('myId'))

    myDiv.testId = 888
    console.log(myDiv.getAttribute('testId'))
Copy the code

Print result:

As you can see from the printed results, custom attributes or features cannot be obtained by non-native creation methods

Note: In HTML5, to comply with the specification, it is recommended to use data- as a prefix for custom attributes. This is a good convention for clearing the distinction between custom features and native features.

12.3 Troublesome style features

Getting and setting style features can be quite a headache compared to getting and setting general features. Just like we looked at features and attributes in the previous section, in this section we deal with style values in two ways: property values, and element attributes created from property values.

The most common is the style element attribute, which is not a string but an object whose attributes correspond to the style specified within the element tag. In addition, we’ll look at apis that give you access to all the computed style information for an element. Computed styles are the application styles of all integrated styles and the actual styles applied to that element after the application styles are evaluated.

12.3.1 Where is the style

The element’s style information is located on the style property of the DOM element, and the initial value is set on the element’s style property. For example, style=”color:red” will save the style information in the style object. During page execution, scripts can set or modify values in style objects, and these changes directly affect the presentation of elements.

  <style>
    div {
      font-size: 24px;
      border: 0 solid gold;
    }
  </style>

  <div style="color: #000000">ninja</div>

  <script>
    document.addEventListener('DOMContentLoaded', () => {
      const div = document.querySelector('div')

      console.log(div.style.color)

      console.log(div.style.fontSize)

      console.log(div.style.borderWidth)
      
      div.style.borderWidth = '4px'
      
      console.log(div.style.borderWidth)
    })
  </script>
Copy the code

Print result:

Tests show that inline styles and newly assigned styles are recorded, but stylesheet inherited styles are not.

Note: Any value in the style property of the element takes precedence over the value inherited by the stylesheet (even if the stylesheet rule uses! Important: (Although rendering effect! Important has the highest priority, which is related to rendering.

12.3.2 Obtaining the Computed Style

The standard approach for all modern browser implementations is the getComputedStyle method. This method receives the element whose style is to be evaluated and returns an interface through which attributes can be queried. The returned interface provides a method called getPropertyValue to retrieve the calculation of a particular style property.

<style> div { background-color: #ffc; display: inline; font-size: 24px ! important; border: 1px solid crimson; color: green; } </style> <div style="font-size: 28px;" id="testSubject" title="Ninja power!" ></div> <script> const div = document.querySelector('div') const computedStyles = getComputedStyle(div) console.log(computedStyles.getPropertyValue('font-size')) console.log(computedStyles.getPropertyValue('display')) console.log(computedStyles.getPropertyValue('border')) console.log(computedStyles.getPropertyValue('border-top-color')) </script>Copy the code

12.3.3 Measure the height and width of elements

Style attributes such as height and width pose another special problem. When no value is specified, they default to auto so that the size of the element depends on its content. Therefore, unless the display provides a property string, we cannot use height and width to get the exact value.

Both offsetHeight and offsetWidth provide fairly reliable access to the height and width of the actual element.

The offsetHeight and offsetWidth of a hidden element are both 0. If we want to get its width and height when it is not hidden, we can temporarily unhide the element, get its value, and then hide it.

Specific methods are as follows:

  1. Set the display property to block
  2. Set visibility to Hidden
  3. Set position to Absolute
  4. Get element size
  5. Restores previously changed properties

12.4 Avoiding Layout Jitter

You’ve learned how to modify the DOM, create and insert new elements, delete existing elements, and modify their attributes. Modifying the DOM is one of the basic tools for implementing highly dynamic Web applications. But this tool also has certain side effects, the most important one being that it can cause layout jitter. Layout jitter occurs when we perform a series of sequential reads and writes to the DOM, and the browser cannot perform layout optimization during this process.

Before delving further, it is important to realize that changing the characteristics of an element does not necessarily affect that element alone; instead, it can lead to a cascade of changes. For example, setting the width of an element can cause its children, siblings, and parents to change. So every time a change is made, the browser must calculate the impact of those changes. In some cases, we can’t do this, and we need to make these changes. At the same time, we should not continue to add to the bad influence of the browser and thus degrade the performance of our Web applications.

Because recalculating the layout is expensive, browsers try to do as little or as little work as possible to delay the layout. They try to batch as many writes to the DOM as possible in the queue, perform all the batch operations, and finally update the layout. But sometimes we write code in a way that doesn’t give the browser enough room to perform these optimizations, and we force a lot of recalculation. This is what causes layout jitters. When our code makes a series of sequential reads and writes to the DOM, the browser can’t optimize the layout. The core problem is that whenever we modify the DOM, the browser must recalculate the layout before reading any layout information. This is a huge loss in performance.

Read layout attributes and write layout attributes in batches.

Apis and properties that cause layout jitter:

React uses the virtual DOM and a set of JavaScript objects to emulate the real DOM for excellent performance. When we develop the application in React, we can perform all the changes to the virtual DOM, regardless of layout jitter. Then, after the fact, React uses the virtual DOM to determine what changes need to be made to the actual DOM to keep the UI synchronized. This innovative batch processing method further improves application performance.

12.5 summary

  • To insert DOM nodes quickly, use DOM fragments because you can inject fragments in a single operation, greatly reducing the number of operations.

  • DOM element attributes and properties, although linked, are not always the same. DOM attributes can be read and written using the getAttribute and setAttribute methods, as well as written using object attribute symbols.

  • When working with properties and features, it is also necessary to understand custom properties. The attributes we customize on DOM elements are for custom information only and cannot be treated or used in the same way as element attributes.

  • The element style is an object that contains attributes that correspond to the style value specified in the element tag. To get a post-computed style that also takes into account the style set in the stylesheet, use the built-in getComputedStyle method.

  • To get the size of an HTML element use offsetWidth and offsetHeight

  • When code makes a series of consecutive reads and writes to the DOM, the browser forces a recalculation of layout information each time, which can cause layout jitter. As a result, Web applications run and respond slowly.

Due to the word limit, a new article: JavaScript Ninja Secrets “Deep Reading Records (ii)