1. Overview – How to write high-performance JavaScript

As the software development industry continues to evolve, performance optimization is an inevitable topic. What kind of behavior counts as performance optimization? Essentially any behavior that can improve operating efficiency and reduce operating overhead can be regarded as an optimization operation, which means that there must be a lot of optimization in the software development process, especially in the front-end application development process. Performance optimizations can be seen everywhere, such as the network used to request resources, the way data is transferred, or the framework used during development. This stage we want to explore the karyotype is the optimization of JS language, specifically from the use of cognitive memory space to the way of garbage collection so that we can write efficient JS code. Specific contents will include the following:

  1. Memory management: Here we first explain why memory needs to be managed and the basic flow of memory management.
  2. Garbage collection in the common GC algorithm;
  3. V8 garbage collection: Learn more about what GC algorithms are used in V8 engines to implement the current garbage collection.
  4. The Performance tools;
  5. Code optimization examples

2, JS Memory Management

With the development of hardware technology in recent years, high-level programming languages also have their own GC mechanism. So these changes, developers do not need to pay special attention to memory space can also be normal to complete the corresponding function development. Why rehash memory management here? The following is a very simple code to illustrate: here, when the FN function is called, it will apply for a relatively large space to the memory as much as possible. And then there’s really no type of problem with executing this function syntactically. But when we use a performance check tool to monitor the script’s memory during execution, we will see that its memory changes just like the blue line shown in the current diagram. It keeps going up, and we don’t see a pullback in the process. This represents a memory leak, and if you don’t understand the mechanics of memory management when you write code, you can write memory problems that aren’t easy to detect. After this kind of code more to the program, may be some unexpected bugs. So master the management of memory, or very necessary.

function fn () {
    arrlist = [];
    arrlist[100000] = 'lg is a coder';
}
fn()
Copy the code

What is memory management?

From the current such a word itself, memory is composed of read and write units, representing a piece of operable space; Management is the initiative to operate a piece of space application, use and release, even if we use some API, but ultimately we can do such a thing autonomously; So memory management is the developer can actively use memory to apply for space, use space, release space, so the process is very simple. There are three steps: apply, use, release.

How is memory management done in JS?

Like any other language, it performs this process in three steps. However, because ES does not provide the corresponding operation API, it cannot actively call the corresponding API to complete the corresponding space management like C and C++ 🈶️ developers. But even so, it doesn’t stop us from demonstrating how the current life cycle of an inner space is done through JS scripts. In JS because there is no direct provision of the corresponding operation API, so it can only be in the JS execution engine to meet the variable definition statement automatically allocate the corresponding space. So in the claim space phase, you define a variable directly; Using space is actually reading and writing variables, such as assignments; Freefall also doesn’t have a freefall API in JS, so it can be used in an indirect way, such as setting the object variable to NULL.

Name = 'Leo' // Free space obj = nullCopy the code

The above is equivalent to a process in accordance with the memory management in JS to achieve such a memory management.

Garbage collection in JS

What content in JS is considered garbage?

In the later GC algorithm, there will also be the concept of garbage which is exactly the same concept. For the front-end development of JS said memory management is automatic, whenever to create an object, array, function when the memory will automatically allocate the corresponding space. Objects are considered garbage if they cannot be found by reference during the execution of subsequent program code. Or the object already exists, but because of some improper syntax or structural error in the code, there is no way to find such an object, which is also considered garbage. Knowing what garbage is, the JS execution engine goes to work and reclaims the object space they occupy. This process is known as JS garbage collection and uses several concepts: reference, access from the root (which is also used frequently in later GC), and reachable objects. To sum up, the characteristics of garbage in JS are:

  • Objects are garbage when they are no longer referenced
  • Objects that cannot be accessed from the root are garbage

What is a reachable object in JS?

Reachable objects are very easy to understand in JS. They are objects that can be accessed. As for how to access it, you can look it up either by reference or by scope chain in the current context. An object is considered reachable as long as it can be found. But there is a small prerequisite, which is that it must be found at the root before it can be considered reachable. So here we are again talking about what is a root? In JS we can think of the current global variable object as the root, which is called the global execution context. Garbage collection in JS is essentially finding garbage and letting the JS execution engine do the space freeing and recycling.

How are references to reachability reflected in JS?

In the following example, we define an object space containing the name attribute. When it is assigned to obj, a reference is generated. Furthermore, in the global execution context, the current OBJ can be found from the root, so the obJ is reachable. This indirectly means that the object space we define is actually a reachable object. After that, we do one more operation to assign the value of obj to the variable ali. After last night’s operation, we can think of the object space as having one more reference, so there is a reference value variation here, which will be used later in the reference counting algorithm. After this operation, we can do a new operation to assign obj to null. After this operation, we can think about, by itself, the object space has two references. With obj pointing to null, is the reference object reachable? It must be, because Ali is still referring to such an object space, so it is still a reachable object. These are the main notes of some of the citations and also see a reachable by the way.

let obj = {
    name: 'leo'
}
let ali = obj
obj = null
console.log(ali)
Copy the code

JS reachable operation case: in order to facilitate the later demonstration of the tag clearing algorithm in GC, so this example will be a little more troublesome to write.

function objGroup (obj1, obj2) {
    obj1.next = obj2
    obj2.prev = obj1
    return {
        o1: obj1,
        o2: obj2
    }
}
let obj = objGroup({name: 'obj1'}, {name: 'obj2'})
console.log(obj)
Copy the code

function objGroup (obj1, obj2) {
    obj1.next = obj2
    obj2.prev = obj1
    return {
        o1: obj1,
        o2: obj2
    }
}
let obj = objGroup({name: 'obj1'}, {name: 'obj2'})
delete obj.o1
delete obj.o2.prev

console.log(obj)
Copy the code

The graph removes all lines where obj1 can be found, which means there is no way to find obj1 in the object space, and the JS execution engine will consider obj1 to be garbage.

It is then found and released and reclaimed. When you write code, you have some kind of object reference relationship, and then you can look underneath the root, and you can follow that line and find an object. But if you’re trying to find this object some of the paths are broken, or recycled. If there is no way to find it again, it will be treated as a garbage object, and finally let the garbage collection mechanism to collect it.

4. GC algorithm introduction

GC is short for garbage collection, which helps us find garbage objects in memory while GC is working. This space can then be freed up and reclaimed for subsequent code to use.

What is the garbage in the GC?

  • Objects that are no longer needed in a program: From the perspective of program requirements, data can be treated as garbage if it is no longer needed in the context after it has been used. For example, the name in the code, when the function call is done, the name is no longer needed here, so from a requirements point of view. It’s supposed to be recycled as garbage, whether it’s actually recycled or not is not up for debate;

    function func () {
      name = 'leo'
      return `${name} is a coder`
    }
    func()
    Copy the code
  • Objects that cannot be accessed in the program: Consider whether this variable can be referenced during the current program execution. For example, in the following case, a name is still defined inside the function. But this time in front of the variable with a declaration of the variable keyword, after such a keyword. When the function call ends, the name is no longer accessible in the outer space. So when we can’t find it, it can actually count as a kind of garbage.

What is the GC algorithm?

The GC is a mechanism where the garbage collector does the actual collection. The essence of the job is to find garbage, free space, and reclaim space, so there are several actions in this process:

  • How to find garbage
  • How do you release space when you release it
  • How to allocate space in the process of recycling

So there have to be different ways of doing this. So the GC algorithm can be understood as the rules that the garbage collector follows in the process of working, like some mathematical formulas.

  • GC is a mechanism where the garbage collector does the specific work
  • The content of the work is to find the garbage release space, recycling space
  • Algorithms are rules that the job is to find and recycle

Common GC algorithms:

  • Reference counting: using a number to determine if the current object is garbage
  • Tag cleanup: A tag can be added to live objects while the GC is running to determine if it is garbage.
  • Tag cleanup: Similar to tag cleanup, but different things will be done later in the collection process;
  • Generational recycle: There will be a recycle mechanism for V8 in the future;

5. Implementation principle of reference counting algorithm

For reference counting algorithms, the core idea is to internally maintain the number of references to the current object through a reference counter. To determine whether the object is a junk object, check whether the reference value of the object is 0. When this value is zero, the GC begins to reclaim and free one of its object Spaces for use. There is a name mentioned here called reference counter, about which there is another little impression. Compared to some other GC algorithms, reference counting may be different from other GC algorithms in terms of execution efficiency due to the existence of reference counters. After that, we need to think a little bit about when the number we’re quoting changes. So here it gives a rule that when a reference relation of an object changes, the reference counter will actively modify the reference value of the current object. What does it mean when a reference relationship changes? For example, we now have an object space in our code, and now we have a variable name pointing to it. At this point we’re going to increment the value by one, and if we have another object at this point pointing to it we’re going to increment the value by one. In the case of a decrease, just subtract one. When such a reference number is found to be zero, the GC immediately works and reclaims the current object space. After that, let’s go through some simple code to illustrate a situation where the reference relationship changes.

const user1 = {age:11}
const user2 = {age:22}
const user3 = {age:33}

const nameList = [user1.age, user2.age, user3.age]
function fn () {
    num1 = 1
    num2 = 2
}
fn()
Copy the code

User1, user2, user3, nameList; From the perspective of variables, num1 and num2 in the fn function are also mounted under the current window object because there is no keyword in front of it. So at this point, none of these variables are going to have a reference count of zero, and then we’re going to make some changes. For example, if we add a keyword declaration before num1 and num2 in the FN function, it means that num1 and num2 can only be used in the fn block-level scope. So once the fn call is done, we can no longer find num1 or num2 in the external global location. The reference count for num1 and num2 will go back to 0, and the GC will start working immediately if it is 0 at this point. Treat them as garbage for an object collection, which means that the memory space in which the functions are executed is reclaimed.

function fn () {
    const num1 = 1
    const num2 = 2
}
fn()
Copy the code

Because all names in the nameList list refer to the user object space, the reference count is not zero, so it will not be garbage collected. This piece is about reference counting algorithm in the implementation process, it follows some basic principles.

Conclusion: The current object is a junk object based on the value of a reference count to determine whether it is 0.

6. Advantages and disadvantages of reference counting algorithms

Advantages of reference counting algorithm:

  • Garbage collection immediately: Because it can determine whether the object is garbage based on whether the current number of references is 0, it can be immediately released if it finds it.
  • Minimize program pauses: An application must consume memory during execution. On our current execution platform, memory is bound to be capped, so memory is bound to be full at some point. Since the reference counting algorithm always monitors objects whose reference value is set to zero, it can be assumed that, to take an extreme case, when it sees that memory is about to be full, the reference counting will immediately find the object space with the value of zero. And then we release it, so that we can make sure that the current memory is not temporarily full, which is what we call reducing the pause of our program.

Disadvantages of the reference counting algorithm:

  • Object that cannot be recycled for loop reference:

    function fn () {
      const obj1 = {}
      const obj2 = {}
    
      obj1.name = obj2
      obj2.name = obj1
    
      return 'leo'
    }
    fn()
    Copy the code
  • Time overhead: the current reference count needs to maintain a value change, so in this case it monitors whether a reference value of the current object needs to be changed. The value itself takes time to change, and if there are more objects in memory to change, the time will be larger. Therefore, compared to other GC algorithms, reference counting algorithm has a higher time overhead.

This section is a brief illustration of the advantages and disadvantages of reference counting algorithms

7. Realization principle of mark clearing algorithm

Compared with reference counting, the principle of token clearing algorithm is simpler to implement. It also solves some problems and will be used a lot in V8. The core idea of the tag removal algorithm is to divide the whole garbage collection operation into two stages.

In the first stage he will go through all the objects and then find the active objects to mark. Live objects are the same as reachable objects;

The second phase, again, goes back through all the objects and removes the ones that are not marked. Need to be aware of is in the second stage at the same time it will be the first phase of the set of tags to erase, to facilitate the normal work of GC next time will also be able to go, so that it can go through the two traverse the behavior, the current such a rubbish space to recycle and eventually to the corresponding free list for maintenance.

This is the realization principle of the tag clearing algorithm, in fact, there are two operations: the first is the tag, the second is to do the cleanup. To make things easier to understand, the following illustration illustrates: in the global place we can find three reachable objects such as A, B and C. Once you find these three reachable objects, you’ll find that they have a few child references underneath them, so this is where the mark-clearing algorithm is powerful. If we find that there are still children or even children below it, it will continue to search for those reachable objects in a recursive way, and D and E in the figure will also be marked as reachable. If a1 and B1 are placed on the right, it may be because we are currently in a local scope, and when the current scope completes, such a space will be reclaimed. Therefore, we cannot find A1 and B1 from the current global chain. In this case, the GC mechanism considers it a garbage object, does not mark it, and eventually finds A1 and B1 and recycles them directly when the GC is working. This is what the tag phase and the cleanup phase of the tag sweep do, which is briefly reorganized into two steps: in the first phase we go to find all the reachable objects, and if there is a hierarchy involved in our application here, it goes back recursively. Just like the process of global looking for A and then D in the picture. Once it’s done, it marks the reachable objects, and when it’s done, it moves on to the second stage. And then it starts doing the cleanup, finding objects that aren’t marked. At the same time, it will also clear the first mark made before it, so that we complete a garbage collection. At the same time, we should also be aware that it will eventually put the reclaimed space directly on the current and a free list, so that later applications can apply for space directly there.

This piece is about the realization principle of the marker clearing algorithm

8. Advantages and disadvantages of the tag clearing algorithm

Advantages of the tag clearing algorithm

Tag clearing has one of the biggest advantages over reference counting. That is, it can solve the previous object recycling operations. For example, we define two objects A1 and b1 in a function and let them refer to each other. Calls to such functions are bound to free up space inside them at the end of the call. So in this case, once a function call ends, the variables in its local space lose a connection to the current global scope. Therefore, a1 and B1 are unreachable in global, which is an unreachable object. Unreachable objects cannot be marked during the marking phase. So then in the second phase, when we go to the current collection, we directly find these unmarked objects and free up the space inside them. This is the tag removal can do, but in the reference count, though the current after the end of this function call it within the a1, b1 didn’t also the way in the global place to visit, but due to the current judgment standard is whether the reference value is 0, so in this case it is no way to release the space of the a1, b1. This is the biggest advantage of the token-clear algorithm over the reference counting algorithm.

Disadvantages of the tag clearing algorithm

In the figure below, we simulate a memory storage situation. We currently search from the root, and another directly reachable object is marked in red at the bottom. There are two regions on its left and right that cannot be found from the root. In this case, it will directly reclaim the corresponding space of the two regions, namely the blue region, in the second round of clearing operation. Then add such a free space to the free list. Subsequent programs can then simply apply for space addresses from the free list, but in this case there is a problem. Any space is made up of two parts, one is the storage of the original information of the space such as its size, address and so on, called the header; The other part is dedicated to storing data, called a domain. After the marked clear recycling operation, there is three words of free space on the left and right sides. Since they are separated by the use space of a reachable object, they are actually scattered after the release is complete that is, the addresses are not contiguous. This is important because addresses are not contiguous so in this case, if we want to claim a space later on. As it happens, the size of the spatial address we want to apply for this time is just 1.5 characters. In this case, if we find the space released by B, we will find that it is 0.5 more and 0.5 less when we look for C. This leads to the biggest problem in current labeling algorithms, which is called space fragmentation.

The so-called spatial fragmentation is due to the fact that the garbage objects currently collected are themselves incoherent on the address. Due to this discontinuity, they are scattered in all corners after recycling. If the new generation space happens to match their size, they can be used directly. Once more or less, they are not suitable for use. So this is one of the disadvantages of the tag clearing algorithm, which we call spatial fragmentation.

Conclusion:

  • Advantages: Compared with reference counting, it can solve the problem that circular references cannot be recycled.
  • Disadvantages: It will cause the problem of space fragmentation and can not maximize the use of space;

9. Realization principle of tag collation algorithm

Like the tag cleanup algorithm, tag cleanup will be used frequently in V8. Tag cleanup can be seen as an enhancement to tag cleanup, because they work exactly the same in the first phase. All objects are iterated over and then the current reachable active object is marked. But in the sweep phase, our tag sweep directly recycles unmarked garbage objects. But the tag cleanup does a cleanup before cleanup, moving objects around so that they are contiguous at the address. In order to understand this process, let’s use a diagram to illustrate it:

Mark active objects: Find reachable objects

Collation operation: Move the live object space together to become a contiguous block on the address

Recycle phase: recycle the whole range on the right side of the active object. After the recycle is completed, you will get the situation as shown in the following figure. This situation has obvious advantages over our previous tag clearing algorithm, because we now do not have large quantities of small scattered Spaces in memory. The reclaimed small space is basically continuous. If we want to apply for it in the subsequent use process, we can maximize the space released by the current memory as much as possible.

This process is the tag cleanup algorithm, which, as mentioned earlier, implements frequent GC operations in the V8 engine in conjunction with tag cleanup.

10. Summary of common GC algorithms

  • Reference counting: The idea is to internally maintain a reference value for each object through a reference counter, and determine whether an object is a junk object by whether the value is zero. To reclaim its garbage space, allowing the garbage collector to reclaim the current space. The advantage is that garbage objects are immediately collected, because a value of 0 triggers the GC to find the space for collection and release. Because of this feature, another advantage of reference counting is to minimize program lag. The garbage collector works whenever the space is about to be temporarily full. Then we free up the memory so that we always have some space available; The disadvantage is that objects with circular references cannot be reclaimed, because such a case means that a reference number in the current object space is never zero. That is, it cannot trigger a current garbage collection operation, but it also has the disadvantage of high resource consumption. Because it has a reference counter, and then it has to change the current reference count every time. And the number of references in this object space can be very large or very small. Anyway, frequent operations have some resource overhead, because we don’t think it’s necessarily that fast;
  • Mark clearing: This algorithm is performed in two phases, first traversing all objects and then marking the current active object. The unmarked objects are then cleared to free up space occupied by the current garbage objects. The advantage is that relative to reference counting, it can reclaim the space of circular references, which reference counting cannot do. The disadvantage is that the current algorithm can not maximize all of its space, so it is easy to avoid fragmentation operations. In addition, tag cleanup does not immediately recycle garbage objects, meaning that even if it finds that an object is unreachable during traversal, it does not clear it until the end. And when it does, the current program actually stops working. So this is also a disadvantage of tag clearing;
  • Tag cleanup: This is similar to tag cleanup, except that there are some prior actions in the cleanup phase. Defragment the current address space first, which has the advantage of eliminating space fragmentation because it has one more defragment operation. The disadvantage, as with tag scavenging, is that garbage objects cannot be collected immediately, so this is also a relative disadvantage to reference counting.

11. Meet V8

It is well known that V8 engine is the most mainstream JS execution engine on the market. Chrome browser and nodeJs platform use V8 engine to execute JS code. The reason WHY JS works so well on both platforms is because V8 is the hero behind the scenes, and this is one of the main selling points of V8. Aside from the excellent memory management mechanism behind this speed, V8 also features just-in-time compilation. Previously, many JS engines required the source code to be specialized into bytecode before it could be executed. V8, on the other hand, can translate the source code directly into the current executable machine code, which is very fast. Another big feature of V8 is that it has an upper limit on memory. The memory size of V8 is set to a number of gigabytes, up to 1.5 gigabytes on 64-bit operating systems. For 32-bit browsers, this value is no more than 800MB. Why did V8 do this? The reasons can be summarized in two aspects:

  • V8 was built for the browser itself, and the existing memory size is sufficient for web applications.
  • V8’s internal garbage collection mechanism also makes this setting very reasonable, because the official test was that when the garbage memory reached 1.5GB, V8’s incremental garbage collection algorithm only took 50ms; If the collection is in the form of non-incremental tags, 1m is required. From a user experience perspective, 1m is a long time, so 1.5GB is the limit here.

V8 garbage collection strategy

In the course of the application, there is a lot of data that is used and this data is divided into raw data and object type data. For these basic primitive types of data, are controlled by the program language itself. So when we refer to recycling here, we still refer to the object data currently living in the heap, so this process is dependent on memory operations and we currently know that memory is capped in V8. So we wanted to know how about if we do in this case down to recycle waste, used for V8 is the thought of generational recycling, specifically how to implement, is the current main memory space is divided into two classes according to certain rules, is called the new generation storage area, another is called the old generation of storage area. With such a classification in place, the next step is to adopt the most efficient GC algorithm for each generation. To perform a collection operation on different objects,

A common GC algorithm in V8

  • Generational recycling
  • Space to copy
  • Mark clear
  • Tag to sort out
  • Mark the incremental

13, V8 how to recycle the new generation of objects

The internal memory allocation of V8 is shown in the following figure: since V8 is based on generational garbage collection, the internal memory space of V8 is divided into two parts as shown in the figure. An internal storage area is divided into a white area on the left and a reddish area on the right. The left side is dedicated to storing new generation objects, and the right side is dedicated to storing old generation objects. We are currently only looking at the garbage collection operations in the new generation storage area, so let’s do a few words here. As you saw earlier, the internal space of our current V8 is split into two parts, with the left side dedicated to storing new generation objects. Such a space is set to 32MB on a 64-bit operating system and 16MB on a 32-bit operating system. After having such a space, in it can go to store the corresponding new generation object. Cenozoic objects actually refer to objects with a short life span, how to define the short life span? For example, if there is a local scope in the current code, the variables in the local scope must be released after execution. And we might have a variable somewhere else, somewhere global. This variable down here at the bottom of the global is definitely not recycled until the program exits. So the Cenozoic is a relatively short-lived variable.

V8 how to complete the new generation object recycling?

The algorithm used in this process is mainly copy algorithm and mark collation algorithm operation. First of all, it will divide the current small space on the left into two parts: From and To, and the two parts are equal in size. The From space becomes the used state, and the To state is the idle state. With these two Spaces, if the code needs To apply for space To be used when executing, the variable object will be allocated To the From space first. Once the From space has been applied to a certain extent, a GC operation is triggered, which then marks the From space with live objects using a mark-up operation. After the live object is found, the collation operation is continued to make the spatial position continuous, so that there is no subsequent spatial fragmentation. After these operations are done, the live object is copied To the To space, which means that there is a copy of the live object From the previous From space. The next is the recycle operation, which is To release and recycle the original From space as a whole, and swap the From space with the To space.

Recovery Details

One of the first things that must come to mind during this process is that the space used by a variable object is also present in the current generation object. This is when promotion occurs, which refers to moving objects from the new generation to the old generation for storage. When can promotion be triggered? There are generally two criteria:

  • If a new generation object is still alive after a GC run, it can be copied to the old generation store for storage.
  • If the To space usage exceeds 25% during the current copy process, the active objects need To be moved To the old generation storage area for storage. And the reason why the limit is 25% is because when you do a reclamation operation in the future you’re going To swap From space with To space, which means that the “To” is going To be “From” and that means that the “To” usage is going To be 80% and eventually it’s going To be a storage space for the live object. If the usage of the To space exceeds a certain limit, then when it becomes in use, the space of the new object will not be enough.

How does V8 recycle old generation objects

Old generation space description

The red area on the right is the afterlife storage area, which also has a memory size limit in V8. It’s 1.4 gigabytes on a 64-bit operating system and 700 megabytes on a 32-bit operating system, so you can store specific data in it. And old generation object actually refers to is the current survival time longer object, as to say which object matter? For example, some variables that are stored in global objects, and some variable data that is placed in closures, as mentioned earlier, may also live for a long time.

How does old generation complete garbage collection?

The garbage collection strategies adopted for the old generation are: tag clearing algorithm, tag sorting algorithm, incremental tag algorithm. Specifically, how to use it? In fact, it mainly adopts the mark clearing algorithm to complete the corresponding garbage space release and recycling. Tagged cleanup is used because of its significant efficiency gains relative to space debris. When will the mark-up algorithm be used? Very simply, if we find that when it wants to move the contents of the new generation area to the current generation storage area and at this point in time, the old generation storage area does not have enough space to store the objects moved from the new generation storage area, which is the promotion mentioned earlier. In this case, token defragmentation is triggered, and some of the previous defragmentation space is reclaimed, which gives us more space to use. When we want to move the new generation of objects to the old generation storage area, if there is a lack of space will trigger the operation of mark sorting. Finally, it will use incremental marking to improve the efficiency of the current collection.

Comparison of recycling strategies between old generation and New generation

The new generation’s garbage collection strategy is to trade space for time: because it uses a copy algorithm, it also means that there is a free space inside it at every moment. Because the Cenozoic storage area itself is small, it has even less space to free up. The amount of space wasted in this area is insignificant compared to the time gain it brings. Why not use this dichotomy in old generation object collection? Very simple, because old generation storage area space is relatively large, if bisected basically there are hundreds of megabytes of space is wasted. Moreover, the old generation stores more data, which consumes much time in the process of replication. Thus, old generation is not suitable to use the copy algorithm to achieve.

How do tag deltas optimize garbage collection?

As shown in the figure, the whole collection operation is divided into two processes, one is the execution of the program and the other is the current garbage collection. To be clear, when garbage collection works, it actually blocks the execution of JS scripts, so there is a gap in the diagram. Marked increments refer to, in simple terms, the entire current garbage collection operation broken up into small steps that combine to complete the entire current collection. So as to replace the garbage collection operation completed in one go before, the benefits of doing so can be seen from this figure is very obvious, the main is that we can realize the garbage collection and program execution alternately completed. It’s not like you can’t do garbage collection when the program executes, and you can’t do garbage collection when the program runs. So the time consumption will be more reasonable, simple to look at the example implementation principle of incremental tag: starting from the left side of the application at run time don’t need to perform garbage collection, once when it triggers garbage collection after this one no matter adopt what kind of algorithm in this, it will go to traversal and manipulation of the tag. Now, it’s important to note that because we’re dealing with the old age area, it’s going to have a traversal and in traversal, it’s going to make a mark that doesn’t have to be done all at once. Because there are direct and indirect reachable operations, that is, if we find the reachable object at the first level, we can stop. Let the program execute for a while, let the program execute for a while and then let our GC mechanism do the second step of marking. It’s going to have some child elements underneath it that are also reachable and it’s going to keep marking. Marking a cycle and then stopping the GC to resume program execution is known as alternate execution. Finally, the tag operation is complete and the garbage collection is completed. After completing the garbage collection operation, the program returns to the place where it should be executed. Although this may make you feel that the current program has paused many times, you should be aware that the largest garbage collection in our entire V8 system takes less than a second to reach 1.5 gigabytes in the form of a non-incremental tag. So the above execution interval is reasonable, and in this way it maximizes many of the previous long pause times directly into smaller segments. This will be better for the user.

Summary of V8 garbage collection

  • V8 engine is a mainstream JS execution engine.
  • V8 internal memory has an upper limit: firstly, it is designed for the browser, secondly, it is determined by its internal garbage collection mechanism, and then its collection time will cause user perception.
  • V8 uses the idea of generational recycling: in this process, memory is divided into old and new generations, and the two regions store different types of data. In addition, different garbage collection strategies are adopted between them. Specifically, copying algorithm and tag sorting algorithm are mainly used in the new generation, while tag clearing, tag sorting and incremental tag are mainly used in the old generation.

16. Performance Tool Introduction

Why use Performance?

The purpose of GC is to make the memory space in the program run in a virtuous cycle, the basis of the so-called virtuous cycle is that we can write code in memory allocation. Since ES does not provide an API for manipulating memory space, there is no standard for determining whether it is reasonable or not, because it is all done by GC. So in this case, we have to find a way to pay attention to a change in the current memory in order to determine whether the memory usage is reasonable. So now we have a tool that gives us a lot more monitoring. In the running process of the program to help us to complete the memory space monitoring operation, this piece of simple summary is to use Performance to monitor the memory changes in the running process of the current program at another moment. With such an operation, when our program memory exception directly to find a way to locate the current problem block of code.

Performance Procedure

  • Open your browser and enter the destination url
  • Go to the Developer tools panel and select Performance
  • Enable the recording function and access the specific page
  • Perform user action and stop recording after a certain period of time
  • Analyze memory record information on the interface

17, the embodiment of memory problems

A memory problem

  • Lazy loading or frequent pausing of pages: The underlying layer is usually accompanied by frequent garbage collection, which occurs frequently because some code in the code is suddenly memory burst;
  • Persistent poor page performance: The underlying tend to think that there is a memory expansion, the so-called memory expansion refers to the current interface in order to achieve the best use of speed, may apply for certain memory space but the size of the memory space is far more than the current device itself can provide a size, this time will be able to perceive a persistent bad performance experience;
  • Page performance gets worse and worse over time: the underlying memory leak usually accompanies this, because in this case there is no problem to begin with. Due to the presence of certain code, memory space may become less and less over time. This is known as a memory leak

18. Several ways of memory monitoring

When there is a memory problem, it can be summarized into three situations:

  • A memory leak
  • Memory ballooning
  • Frequent garbage collection

When these problems arise, what criteria should we use to define them? Here are some simple instructions:

  • Memory leak: Memory usage continues to rise. As you can see on the memory chart, memory changes are always rising and do not decrease.
  • Memory bloat: A problem on most devices. Bloat is meant to be the current application itself in order to achieve optimal results, it requires a large amount of memory. Therefore, in such a process, the hardware of the current device itself may not support, resulting in some performance differences in the use of the process. So if we want to determine whether it’s a program problem or a device problem, we should do some more testing.
  • Frequent garbage collection: Analyzed by memory variation graph

Several ways to monitor memory:

  • Browser task manager
  • Timeline Indicates the Timeline record
  • Heap snapshot looks for detached DOM
  • Determine if there is frequent garbage collection

19. Task Manager monitors memory

As we have seen before, there are many ways to observe memory changes in a Web application during execution. Here, for example, is a simple Demo of how to monitor memory changes during script execution with the help of the browser’s built-in task manager.

Just a quick note before we get started, in the future we’re going to be running files in a browser so we’re going to be writing an HTML document. As for how to simulate memory changes in the code, we chose to place an element in the interface and add a click event to it. When this event is triggered, we create a very long array, resulting in a memory consumption.

<html>
<body>
    <button id="btn">Add</button>
    <script>
        const btn = document.getElementById('btn');
        btn.onClick = function () {
            let arr = new Array(100,000)
        }
    </script>
</body>
</html>
Copy the code

The memory manager shown in the figure has a column of memory and a column of JavaScript memory. What’s the difference between the two columns called memory? For a brief explanation, this is native memory for the first column, which means that there are a lot of DOM nodes in the current interface, and this memory refers to the memory occupied by the DOM node. If this number continues to increase, it means that our interface is constantly creating new DOM; And then the last column is called JavaScript memory, and what we’re talking about in this column is the JS heap and what we’re really looking at in this column is the value in the little brackets, which is the amount of memory that all of the reachable objects in our interface are using. If this number keeps increasing it means that either new objects are being created in our current interface or existing objects are growing. For example, on the current screen, the value in the parentheses remains the same until you click. When we look at the change in THE JS memory in the browser task manager after clicking the Add button several times on the page, we see that the value has increased. Through this process we can monitor the entire memory changes while the script is running with the help of the current browser’s task manager. So what we can conclude is that if we’re doing the next column and the values in the parentheses are going up and up, that means that we’re having a problem with memory and in particular, that our current tool is not working as well as it should. Because it can only help us find out if there is a problem with the place, but it doesn’t work very well if we think about status issues.

20. TimeLine record memory

Previously, the browser task manager was used to determine if there was a memory problem, but if you wanted to locate the problem with specific scripts, the task manager was not very useful. So locating the problem also uses the time log memory change method to demonstrate how we can more accurately locate the current memory problem with which piece of code or at what time node it occurred.

Here we are scripting, first of all we will do a few operations: first place a DOM node and then add a click event; The second is to do two things inside JS after the event is triggered: 1. Simulate memory consumption by creating a large number of DOM nodes; 2. Using arrays in conjunction with other methods to form a very, very long string also mimics the current high memory consumption.

<html>
<body>
    <button id="btn">Add</button>
    <script>
        const arrList = [];
        function test () {
            for (let i = 0; i < 1000000; i++) {
                document.body.appendChild(document.createElement('p'))
            }
            arrList.push(new Array(10000000).jion(x))
        }
        document.getElementById('btn').addEventListenter('click', test)
    </script>
</body>
</html>
Copy the code

The heap snapshot looks for detached DOM

How the heap snapshot function works: first find the JS heap and then save it as a photo. Once you have a photo, you can see all the information in it, and that’s one of the reasons why we monitor it. This heap of snapshots is actually quite useful when used, as it is more of a lookup behavior that is specific to our separate DOM.

What is DOM separation?

  • Interface elements live in the DOM tree: The elements we see on the interface are actually DOM nodes, and these DOM nodes are supposed to exist in a living DOM tree, but DOM nodes can take several forms. The first form is what we call junk objects and there’s another called detached DOM;
  • DOM node for garbage object: If the node is removed from the current DOM tree and no one references it in the JS code, it is actually garbage.
  • Detached DOM: If the current node is only detached from the DOM tree, but someone still references it in JS code, this DOM is called detached DOM;

This separation of the DOM is not visible in the interface, but it takes up space in memory, so it is a memory leak in this case. So, we can use the heap snapshot to take it all out of there and if we can find it we can go back to the code and clean it up, so that we can free up some of our current memory.

<html>
<body>
    <button id="btn">Add</button>
    <script>
        let tempEle;
        function fn () {
            const $ul = document.createElement('ul');
            for (let i = 0; i < 10000000; i++) {
                const $li = document.createElement('li');
                $ul.appentChild($li)
            }
            tempEle = $ul
        }
        document.getElementById('btn').addEventListenter('click', fn)
        tempEle = null
    </script>
</body>
</html>
Copy the code

22. Determine whether frequent GC exists

Why frequent garbage collection?

The current application is stopped while the GC is working. So if our current GC is working too frequently and for too long, it’s not very friendly for our WEB application. It will be in a state of suspended animation, and the user will feel that the application is stuttering. This is the time to find out if the current application is executing with frequent garbage collection. There are several ways to determine if there is frequent garbage collection in an application:

  • Frequent ups and downs in timeLine
  • Frequent data increases and decreases in task manager

23, Performance summary

The Performance using

  • Performance Usage Process
  • Analysis of memory problems: memory bloat, memory leak, frequent GC
  • The Performance sequence diagram monitors memory changes
  • The browser task manager monitors memory changes
  • Heap snapshot looks for detached DOM

24. Code optimization introduction

How to accurately test JS performance?

  • Precision testing is essentially a matter of collecting a large number of execution samples for mathematical statistics and analysis: to produce a comparative result to prove which scripts execute more efficiently. And such a process for our coding, seems to be a bit of trouble, because we are more concerned with the use of scripts to achieve a certain function. Instead of doing a lot of math;
  • Using jsperf.com with basic benchmark.js;

Jsperf usage process:

  • Log in using your GitHub account
  • Personal Confidence (optional)
  • Fill in detailed test case information (Title, SLUG)
  • Fill in the prep code (often used in DOM manipulation)
  • Setup and teardown codes are required
  • Fill in the test snippet

25. Be careful with global variables

If some data needs to be stored during program execution, it should be placed in the local scope as a local variable whenever possible.

Why should it be used sparingly?

  • Global variables defined in the global execution context, which is all of the top of the scope chain: the context is the process in the process of search data all at the very top of the scope chain if the search process, according to level up some local scope is not found under the variable would eventually find the top to the current global execution context. In this case, the search time is very high, which reduces the efficiency of the current code;

  • The global execution context remains in the context execution stack until the program exits: variables defined in the current global context remain in the context execution stack until the current program exits. This is also detrimental to the current GC effort, as our GC will not recycle such a variable as a garbage object whenever it finds it alive. So this will also reduce the memory usage of our current program;

  • The presence of a variable with the same name in a local scope obscures or contaminates the whole world:

    Var I, STR = “; for (i = 0; i < 1000; I ++) {STR += I} for (let I = 0; i < 1000; i++) { let str = ”; str += i }

26. Cache global variables

Global variables that cannot be avoided in use are cached locally

The use of cache global variables can make JS code in the execution of the time will have a higher performance, about the cache global variables actually refers to the use of global variables in the process of program execution is unavoidable. A large number of reusable global variables are placed in the local scope to achieve a caching effect.

<html> <body> <input type="button" value="btn" id="btn1" /> <input type="button" value="btn" id="btn2" /> <input type="button" value="btn" id="btn3" /> <input type="button" value="btn" id="btn4" /> <input type="button" value="btn" id="btn5" /> <input type="button" value="btn" id="btn6" /> <input type="button" value="btn" id="btn7" /> <input type="button" value="btn" id="btn8" /> <input type="button" value="btn" id="btn9" /> <input type="button" value="btn" id="btn10" /> <input type="button" value="btn" id="btn11" /> <input type="button" value="btn" id="btn12" /> <input type="button" value="btn" id="btn13" /> <input type="button" value="btn" id="btn14" /> <input type="button" value="btn" id="btn15" /> <input type="button" value="btn" id="btn16" /> <input type="button" value="btn" id="btn17" /> <input type="button" value="btn" id="btn18" /> <input type="button" value="btn" id="btn19" /> <input type="button" value="btn" id="btn20" /> <input type="button" value="btn" id="btn21" /> <input type="button" value="btn" id="btn21" /> <input type="button" value="btn" id="btn22" /> <input type="button" value="btn" id="btn23" /> <input type="button" value="btn" id="btn24" /> <script> function getBtn() { let oBtn1 = document.getElementById('btn1') let oBtn2 = document.getElementById('btn2') let oBtn3 = document.getElementById('btn3') let oBtn4 = document.getElementById('btn4') let oBtn5 = document.getElementById('btn5') let oBtn6 = document.getElementById('btn6') let oBtn7 = document.getElementById('btn7') let oBtn8 = document.getElementById('btn8') let oBtn9 = document.getElementById('btn9') let oBtn10 = document.getElementById('btn10') let oBtn11 = document.getElementById('btn11') let oBtn12 = document.getElementById('btn12') let oBtn13 = document.getElementById('btn13') let oBtn14 = document.getElementById('btn14') let oBtn15 = document.getElementById('btn15') let oBtn16 = document.getElementById('btn16') let oBtn17 = document.getElementById('btn17') let oBtn18 = document.getElementById('btn18') let oBtn19 = document.getElementById('btn19') let oBtn20 = document.getElementById('btn20') let oBtn21 = document.getElementById('btn21') let oBtn22 = document.getElementById('btn22') let oBtn23 = document.getElementById('btn23') let oBtn24 = document.getElementById('btn24') } function getBtn2 () { let obj = document let oBtn1 = obj.getElementById('btn1') let oBtn2 = obj.getElementById('btn2') let oBtn3 = obj.getElementById('btn3') let oBtn4 = obj.getElementById('btn4') let oBtn5 = obj.getElementById('btn5') let oBtn6 = obj.getElementById('btn6') let oBtn7 = obj.getElementById('btn7') let oBtn8 = obj.getElementById('btn8') let oBtn9 = obj.getElementById('btn9') let oBtn10 = obj.getElementById('btn10') let oBtn11 = obj.getElementById('btn11') let oBtn12 = obj.getElementById('btn12') let oBtn13 = obj.getElementById('btn13') let oBtn14 = obj.getElementById('btn14') let oBtn15 = obj.getElementById('btn15') let oBtn16 = obj.getElementById('btn16') let oBtn17 = obj.getElementById('btn17') let oBtn18 = obj.getElementById('btn18') let oBtn19  = obj.getElementById('btn19') let oBtn20 = obj.getElementById('btn20') let oBtn21 = obj.getElementById('btn21') let oBtn22 = obj.getElementById('btn22') let oBtn23 = obj.getElementById('btn23') let oBtn24 = obj.getElementById('btn24') }  </script> </body> </html>Copy the code

Add additional methods through the prototype object

Methods needed to add an instance object to a prototype object

In JS, some content can be related to the prototype chain, so that JS code will have some higher performance when executing. There are three concepts in JS:

  • The constructor
  • A prototype object
  • Instance objects

Instance objects and constructors can both execute prototype objects. If a constructor has a member method inside it that the instance objects need to be called frequently, we can add it to our prototype objects without putting it inside the constructor. There are also performance differences between these two implementations.

// Constructor var fn1 = function () {this.foo = function () {console.log(1111)}} let f1 = new fn1( function () {} fn2.prototype.foo = function () { console.log(22222) } let f2 = new fn2()Copy the code

28. Avoid closure traps

Closure features

  • The outside has a reference to the inside

  • Access data from the inner scope in the outer scope

    function foo () {
      var name = 'leo';
      function fn () {
        console.log(name)
      }
      return fn
    }
    var a = foo()
    a()
    Copy the code

About the closure

  • Closures are a powerful syntax

  • Closures are prone to memory leaks when used improperly

  • Don’t close for closure’s sake

    <html> <body> <button id="btn"> Add </btton> <script> function foo () { var el = document.getElementById('btn') El.onclick = function () {console.log(el.id)} el = null // Avoid closure traps} foo() </script> </body> </ HTML >Copy the code

29. Avoid attribute access methods

Object orientation in JavaScript

  • JS does not require access methods for properties; all properties are externally visible

  • Using attribute access methods only adds another layer of redefinition without access control

    Function Persong () {this.name = ‘Leo ‘; this.age = 18; This.getage = function () {return this.age}} const p1 = new Person() const age1 = p1.getage ( () { this.name = ‘leo’; this.age = 18 } const p2 = new P() const age2 = p2.age

30, For loop optimization

const arr = [1, 2, 3, 4 .... , 10000];
for (let i = 0; i < arr.length; i++) {
    console.log(i)
}
for (let i = 0, len = arr.length; i < len; i++) {
    console.log(i)
}
Copy the code

Choose the optimal loop method

In our actual application development process, we often encounter the traversal structure of data. Given a large number of these data structures, we often have multiple options for traversing them. Foreach, for… When we iterate over a set of the same data, which of these three methods is more efficient in practice?

var arrList = new Array(1, 2, 3, 4, 5); ForEach (function(item){console.log(item)}) // for (var I = arrlist. length; i; i--) { console.log(i) } // for ... in for (var i in arrList) { console.log(arrList) }Copy the code

32. Document fragmentation optimization node added

For Web application development, DOM node operations are very frequent. Interacting with the DOM can be very performance intensive, especially when creating a new node to add to the interface. This process is usually accompanied by backflow and redraw, which have a high performance cost.

<html> <body> <button id="btn"> Add </btton> <script> for (var i = 0; i < 10; I++) {var oP = document. The createElement method (' p ') oP. InnerHTML = I document. The body. The appendChild (oP)} / / document fragments way const fragEle = document.createDocumentFragment() for (var i = 0; i < 10; i++) { var oP = document.createElement('p'); oP.innerHTML = i fragEle.appendChild(oP) } document.body.appendChild(fragEle) </script> </body> </html>Copy the code

Clone optimized node operation

Operations on the current node, such as adding a new node, may be followed by the addition of many attributes and methods. Here, the operation behavior of optimization is still completed by adding nodes and cooperating with cloning.

<html> <body> <p id="box1">old</p> <script> for (var i = 0; i < 10; I++) {var oP = document. The createElement method (' p ') oP. InnerHTML = I document. The body. The appendChild (oP)} / / clone + document fragments way const oldP = document.getElementById('box1') const fragEle = document.createDocumentFragment() for (var i = 0; i < 10; i++) { var newP = oldP.cloneNode(flase); newP.innerHTML = i fragEle.appendChild(newP) } document.body.appendChild(fragEle) </script> </body> </html>Copy the code

33. Replace Object directly

The idea of replacing Oject with a direct quantity is that when we define some object or array. There are two different forms: using new, using literals;

var arr = [1, 2, 3]
var a1 = new Array(3)
a1[0] = 1
a1[1] = 2
a1[2] = 3
Copy the code

JS execution process in stack

Let’s take a look at what happens at the bottom of JS code execution. This gives a more concrete representation of how a piece of code is executed in stack memory and heap memory, and also helps us understand how GC retrieves memory.

let a = 10;
function foo (b) {
    let a = 2;
    function baz (c) {
        console.log(a + b + c)
    }
    return baz
}
let fn = foo(2)
fn(3) // 7
Copy the code

Stack memory ECStack Execution environment stack: Used to store execution context

  • EC baz

  • this = window

  • <baz.ao, foo.ao, vo>

  • AO:

  • arguments(0:3)

  • c = 3

  • console.log(a + b + c)

  • 3 plus 2 plus 2

  • EC foo

  • This = window this points to

  • <foo.ao, vo> Scope chain

  • AO:

  • arguments:(0: 2)

  • b = 2

  • a = 2

  • baz = AB2 [[scop]] foo.AO

  • EC G global execution context:

  • VO:

  • A = 10 is stored in stack memory

  • Foo = AB1 [[scop]] VO Reference type

  • fn = AB2

  • fn (3)

After this process is finished, it means that our current whole process is completed, and the rest of the stack area is called JS main thread for recycling.

Heap memory:

  • AB1 :

  • function foo (b) { … }

  • name: foo

  • Length: 1 // Number of parameters

  • AB2:

  • function bax (c) { … }

  • name: baz

  • Length: 1 // Number of parameters

The space occupied by the contents of the heap is reclaimed by the GC garbage collection mechanism. According to the analysis of the main thread, every time the code in the execution context completes execution, the heap is checked to see whether the contents of the heap are reclaimed. For example, when code in baz is executed, all of its variables in the current scope are not referenced in any other scope after execution. Therefore, at this time, the space must be reclaimed, which is called out of the stack. At this time, after completing the process, the result is 7.

Summary: JS code will first create an execution environment stack in the heap memory after execution, and used to store different execution context; EC G is the global execution context, in which some of the code under the global scope are declared and stored. There is the basic type of data is directly stored in the stack memory, for reference type of data stored in the heap area and GC to recycle processing, and the stack content is out of the stack are processed by the JS main thread. Each time a function is executed, a new execution context is generated on the stack. After the code is executed, the closure is generated to determine whether the heap referenced in the current context should be released.

Reduce the level of judgment

In the process of writing code, it is possible to have this kind of judgment condition escape scenario. And often after this kind of if… Else when fleeing, we can optimize the hierarchy by returning such invalid conditions early.

Function doSomething (part, charpter) {const parts = ['ES2016', 'Vue', 'React', const parts = ['ES2016', 'Vue', 'React', 'Node'] if (part) {if (parts.includes(part)) {console.log(' belongs to the current course ') if (charpter > 5) {console.log(' please provide VIP status ')}} } else {console.log(' console.log ')}} function doSomething (part, charpter) {if (! Part) {console.log(' Please confirm module information ') return} if(! Parts.includes (part)) return console.log(' includes ') if (charpter > 5) {console.log(' please provide VIP status ')}} doSomething('ES2016', 6)Copy the code

Reduce the scope chain lookup level

Var name = 'Leo '; function foo () { name = 'D.Leo'; function baz () { var age = 38; Console. log(age) console.log(name)} baz()} function foo () {var name = 'd.eo '; function baz () { var age = 38; console.log(age); console.log(name) } } foo()Copy the code

37. Reduce the number of data reads

In JS frequently used data representation is mainly divided into: literal, local variables, array elements, object attributes, for these four forms of access literal and local variables speed is the fastest. Because they can be stored directly on the stack, accessing array elements and object members is relatively slow. This is because you need to find their location in the heap memory by reference. For example, the access of object attributes in the operation, often have to consider the search on the prototype chain, so the truth is the same as the scope chain. To reduce the time consumption of the query, we should reduce the number of object member look-up times and the nesting level of attribute members as much as possible. One trick is to cache the data to be used in advance for later use.

< HTML >< body> <div id="skip" class="skip"></div> <script> Function hasEle (ele, CLS) {return els.className === CLS} cls) { var classname = ele.className return classname === cls } </script> </body> </html>Copy the code

Literals and constructions

// constructor let test = () => {let obj = new Object(); obj.name = 'leo'; age: 18; Const STR = new String('I\'m String ') // let test = () => {let obj = {name: 'Leo ', age: 18 } return obj } const str = 'I\'m string'Copy the code

39, reduce circulation body activity

In the case of a fixed number of loops, more things in the body of the loop means slower execution, and vice versa. Statements that use the same data value for each operation can be pulled out of the loop body, similar to data caching.

// const arr = [1, 2, 3, 4, 5]; for (let i = 0; i < arr.length; I ++) {console.log(I)} const len = arr.length for (let I = 0; i < len; I ++) {console.log(I)} let len = arr.length while (len--) {console.log(arr[len])}Copy the code

40, Reduce the number of declarations and statements

<html> <body> <div id="box" style="width:100px; Var oBox = dovument.getelementbyId ('box'); Function test (ele) {let w = ele.offsetwidth let h = ele.offsetheight return w*h} Return ele.offsetwidth * ele.offsetheight} console.log(test(oBox)) function test() {var name = 'test'; var age = 18; var slogen = 'DBHFHNF'; Return slogen + age + solgen} function test () {var name = 'test', age = 18, slogen = 'DBHFHNF'; return name + age + solgen } </script> </body> </html>Copy the code

41. Use event delegation

The essence of event delegation is to use the mechanism of event bubble to delegate the response event that should be bound to the child element to the parent element, so that the parent element completes the monitoring of the event. The advantage of doing this is that you can significantly reduce the memory footprint and thus the event registration.

< HTML > < body > < ul id = "box" > < li > 1 < / li > < li > 2 < / li > < li > 3 < / li > < / ul > < script > / = const list before optimization document.querySlectorAll('li') function showText (ev) { console.log(ev.target.innerHTML) } for (let item of list) { Item.onclick = showTxt} // Const oUl = document.getelementById ('ul'); oUl.addEventListener('click', showTxt, true); function showTxt (ev) { const obj = ev.target if (obj.nodeName.toLowerCase === 'l1') { console.log(obj.innerHTML) } } </script> </body> </html>Copy the code