[Front-end skill tree - pits to avoid] Javascript developers easily make mistakes in the flower field

JavaScript is already at the core of almost all modern web applications. Building basic JavaScript functionality into a web page is a fairly simple task, even if they are new to JavaScript. However, due to the flexibility and subtlety of Javascript itself, developers (especially junior developers) often face some problems caused by Javascript.

In this article, I will discuss 10 of them with you. It's important to understand and avoid these problems if you want to become a full-fledged JavaScript developer.

1. this: wrong reference

Self-referential scopes in callbacks and closures in JavaScript are often used in design patterns and are a fairly common source of "mess" that causes JavaScript problems.

For example the following piece of code:

const Game = function() {
    
    
    this.clearLocalStorage = function() {
    
    
        console.log("clear storage");
    };
    this.clearBoard = function() {
    
    
        console.log("clear board");
    };
};

Game.prototype.restart = function () {
    
    
    this.clearLocalStorage();
    this.timer = setTimeout(function() {
    
    
        this.clearBoard();  // this
    }, 0);
};

const myGame = new Game();
myGame.restart();

Executing the above code results in the following error:

Uncaught TypeError: this.clearBoard is not a function

Why would it cause such an error? Everything depends on your development/production environment. The reason you're getting this error is because, when you call setTimeout(), you're actually calling window.setTimeout(). setTimeout()Therefore, the anonymous function passed to is windowdefined in the context of the object, which has no clearBoard()method.

A traditional solution is to simply thiskeep your reference to in a variable that can be inherited by the closure, for example:

Game.prototype.restart = function () {
    
    
    this.clearLocalStorage();
    const self = this;   // 保存在一个变量中
    this.timer = setTimeout(function(){
    
    
        self.clearBoard();    // 通过
    }, 0);
};

Alternatively, you can use bind()the method to pass the correct reference:

Game.prototype.restart = function () {
    
    
    this.clearLocalStorage();
    this.timer = setTimeout(this.reset.bind(this), 0);  // bind
};

Game.prototype.reset = function(){
    
    
    this.clearBoard();    // 通过

2. Block-level scope

A common bug among JavaScript developers is to assume that JavaScript creates a new scope for each block of code. While this is true in many other languages, it's not true in JavaScript. For example, the following piece of code:

for (var i = 0; i < 10; i++) {
    
    
    /* ... */
}
console.log(i);  

If you guessed console.log()output undefined, or an error was thrown, then you guessed wrong. Because, it will output 10. Why?

In most other languages, the code above would result in an error like this. Because ithe "life cycle" (that is, the scope) of the variable is limited to forthe loop statement. But in JavaScript, this is not the case, even after forthe loop completes, the variable iremains in scope, retaining its last value after exiting the loop . (This behavior is known as variable hoisting .)

There is a solution. letBlock scoping is supported in JavaScript through the keyword.

3. Memory leak

Memory leaks are an almost inevitable problem in JavaScript. There are many ways in which they can happen, so here I just want to highlight two of the more common ones for you.

3.1 Null references to invalid objects

Although this example only applies to older JavaScript engines (since modern engines have garbage collectors smart enough to handle this situation), I still want to emphasize it.

For example the following piece of code:

var theThing = null;
var replaceThing = function () {
    
    
  var priorThing = theThing;  
  var unused = function () {
    
    
    if (priorThing) {
    
    
      console.log("hi");
    }
  };
  theThing = {
    
    
    longStr: new Array(1000000).join('*'),  
    someMethod: function () {
    
    
      console.log(someMessage);
    }
  };
};
setInterval(replaceThing, 1000);  

When you run the above code and monitor the memory usage, you will find that there is a serious memory leak problem. Even a manual garbage collector doesn't help. replaceThingIt looks like we're leaking every time we call longStr. But why?

Let's re-examine this code in more detail and find:

Each theThingobject contains objects of 1MBsize longStr. Every second, when we call replaceThing, it saves a reference to the priorThingprevious theThingobject in . But we still don't think that's a problem, because every time you pass a previously referenced priorThingwill be dereferenced. Also, it's only replaceThingreferenced in the body of the and unused functions, which are never actually used.

So wondering again why there is a memory leak here.

In order to understand what's going on, we need to better understand the inner workings of JavaScript. Closures are typically implemented by each function object linked to a dictionary object representing its lexical scope. If replaceThingtwo functions defined inside are actually used priorThing, then they both get the same object, even priorThingif is assigned repeatedly so that both functions share the same lexical environment. However, once a variable is used by any closure, it enters the lexical environment shared by all closures in that scope . It's this nuance that causes this severe memory leak.

3.2 Circular references

The following piece of code:

function addClickHandler(element) {
    
    
    element.click = function onClick(e) {
    
    
        alert("Clicked the " + element.nodeName)
    }
}

Here, onClickthere is a closure that element.nodenamekeeps elementa reference to by . After the click is triggered, a circular reference is created, i.e.element→onClick→element→onClick→element…

Interestingly, elementthe circular reference above prevents elementand onClickfrom being collected even after being removed from the DOM, resulting in a memory leak.

So, how to avoid it? Then look down.

3.3 Avoid memory leaks

JavaScript's memory management (especially its garbage collection) is largely based on the concept of object reachability.

The following objects are considered reachable:

  • Objects referenced from anywhere in the current call stack (i.e. all local variables and parameters in the currently called function, and all variables in the scope of closures)
  • all global variables
  • Objects are kept in memory as long as they are reachable from any root through a reference or chain of references

There is a garbage collector in the browser to clean up the memory occupied by unreachable objects. In other words, objects are removed from memory if and only if the GC deems them unreachable . Unfortunately, it's very easy to get "zombie" objects that are no longer used, but are still considered reachable by the GC.

4. The confusion of the equal sign

One of the conveniences of JavaScript is that it automatically coerces any value referenced in a boolean context to a boolean. But in some cases, this practice is both convenient and confusing. For example, the following expression is cumbersome for many JavaScript developers:

console.log(false == '0'); // true
console.log(null == undefined); // true
console.log(" \t\r\n" == 0); // true
console.log('' == 0); // true

// true
if ({
    
    }) // ...
if ([]) // ...

As for the last two, despite being empty, and {}are []actually objects, and any object will be coerced to a boolean in JavaScript true, ECMA-262consistent with the spec.

Therefore, unless a type cast is explicitly required, it's usually better to use ===and !==instead of ==and !=to avoid any unintended side effects of the type cast. Because, ==and !=automatically perform type conversion when comparing two things, and ===and !==perform the same comparison without type conversion.

Since we're talking about type casts and comparisons, it's worth mentioning that comparing NaNwith anything (even ) always returns . Therefore, the equality operator ( ) cannot be used to determine whether a value is . Instead, the built-in global function should be used :NaN!false==、===、!=、!==NaNisNaN()

console.log(NaN == NaN);    // false
console.log(NaN === NaN);   // false
console.log(isNaN(NaN));    // true

5. Inefficient DOM manipulation

While it is relatively easy to manipulate the DOM (for example, add, modify, and delete elements) using JavaScript, it does not improve the efficiency of the operation.

A common example is code that adds one DOM element at a time. Adding DOM elements is an expensive operation, and code that adds multiple DOM elements in a row is inefficient and probably won't work well.

When multiple DOM elements need to be added, an effective alternative is to use document fragments ( document fragments ), which will effectively improve efficiency and performance:

const div = document.getElementById("my_div");
const fragment = document.createDocumentFragment();
const elems = document.querySelectorAll('a');

for (let e = 0; e < elems.length; e++) {
    
    
    fragment.appendChild(elems[e]);
}
div.appendChild(fragment.cloneNode(true));

6. Wrong use of function definition in for loop

Consider this code:

const elements = document.getElementsByTagName('input');
const n = elements.length;    
for (var i = 0; i < n; i++) {
    
    
    elements[i].onclick = function() {
    
    
        console.log("This is element #" + i);
    };
}

According to the code above, if there are 10 input elements, clicking any of them will display "This is element #10"! This is because, when calling on any element, the above onclickloop forhas already completed and ithe value of is already 10 .

Let's fix this problem:

const elements = document.getElementsByTagName('input');
const n = elements.length;    
var makeHandler = function(num) {
    
      
     return function() {
    
    
         console.log("This is element #" + num);
     };
};
for (var i = 0; i < n; i++) {
    
    
    elements[i].onclick = makeHandler(i+1);
}

In this modified version of the code, each pass through the loop is executed immediately makeHandler, each time receiving the value at that time i+1and binding it to a scoped numvariable. The outer function returns the inner function (which also uses this scoped numvariable), and the element's onclickis set to the inner function. By scoping numthe variable, ensure that each onclickreceives and uses the correct i value.

7. Failure to properly utilize prototypal inheritance

Quite a few JavaScript developers don't fully understand the features of prototypal inheritance, and therefore don't take full advantage of it.

Here's a simple example:

BaseObject = function(name) {
    
    
    if (typeof name !== "undefined") {
    
    
        this.name = name;
    } else {
    
    
        this.name = 'default'
    }
};

This seems fairly simple. If one is provided name, this is used name, otherwise nameset to 'default'. For example:

var firstObj = new BaseObject();
var secondObj = new BaseObject('unique');

console.log(firstObj.name);  // ->  'default'
console.log(secondObj.name); // ->  'unique'

But what if we do:

delete secondObj.name;

we will get undefined:

console.log(secondObj.name); // -> 'undefined'

This is easy to do if we modify the original code to take advantage of prototypal inheritance, as follows:

BaseObject = function (name) {
    
    
    if(typeof name !== "undefined") {
    
    
        this.name = name;
    }
};

BaseObject.prototype.name = 'default';

In this version, the property BaseObjectis inherited from its prototype object name, where it is set to 'default'. Therefore, if the constructor is called without a name, the name will default to default. Similarly, if namethe property BaseObjectis removed from an instance of , then the prototype chain will be searched and namethe property will be retrieved from the prototype object with the value still 'default' . Now we get:

const thirdObj = new BaseObject('unique');
console.log(thirdObj.name);  // -> 'unique'

delete thirdObj.name;
console.log(thirdObj.name);  // -> 'default'

8. Incorrect reference to instance method

Let's define a simple object and create its instance as follows:

const MyObjectFactory = function() {
    
    }
	
MyObjectFactory.prototype.whoAmI = function() {
    
    
    console.log(this);
};

const obj = new MyObjectFactory();

Now, for convenience, let's create a whoAmIreference to the method so we can access it with just whoAmI()instead of the longer :obj.whoAmI()

const whoAmI = obj.whoAmI;

To make sure we've stored a reference to the function, let's print out whoAmIthe value of the new variable:

console.log(whoAmI);

output:

function () {
    
     
    console.log(this); 
}

Looks good so far.

But, let's see the difference when we call obj.whoAmI()and :whoAmI()

obj.whoAmI();  // "MyObjectFactory {...}" 
whoAmI();      // "window"

What's wrong?
Our whoAmI()call is in the global space, so it's being set to window(or, in strict mode, to undefined), not an instance MyObjectFactoryof obj!

In other words, the value of this usually depends on the calling context.

However, now there is a new way. Since arrow functions ((params) =>{})provide a static this , it's not based on the calling context like regular functions , and we can handle this with arrow functions:

const MyFactoryWithStaticThis = function() {
    
    
    this.whoAmI = () => {
    
     
        console.log(this);
    };
}

const objWithStaticThis = new MyFactoryWithStaticThis();
const whoAmIWithStaticThis = objWithStaticThis.whoAmI;

objWithStaticThis.whoAmI();  // "MyFactoryWithStaticThis" 
whoAmIWithStaticThis();      // "MyFactoryWithStaticThis" 箭头函数起效了!

9. String as the first parameter of setTimeout or setInterval

For starters, let's get something clear here: having a string as setTimeout 或setIntervalthe first argument to is not itself an error. It's perfectly legal. The issue here is more of a performance and efficiency issue.

One thing we often overlook is that if you pass a string as the first parameter to setTimeout or setInterval, it will be passed to the function constructor to be converted to a new function . This process can be slow and inefficient . For example the following piece of code:

setInterval("logTime()", 1000);
setTimeout("logMessage('" + msgValue + "')", 1000);

Your better option, then, is to pass in a function as an initial argument , for example:

setInterval(logTime, 1000);   // 传入函数 logTime
	
setTimeout(function() {
    
           // 传入匿名函数
    logMessage(msgValue);   
}, 1000);

10. Not using "strict mode"

"Strict mode" is a method of voluntarily enforcing stricter parsing and error handling of JavaScript code at runtime, and a method of making code more secure.

Not using strict mode is not really a "mistake", but its use is increasingly encouraged.

Below I summarize some of the main benefits of strict mode:

  • Make debugging easier. Code errors that would otherwise be ignored or fail silently now generate errors or throw exceptions, alerting you sooner and leading you to their source sooner.
  • Prevent accidental global variables. In the absence of strict mode, assigning a value to an undeclared variable automatically creates a global variable with that name . This is one of the most common JavaScript errors. In strict mode, attempting to do so will throw an error.
  • Without strict mode, references to a this value of null or undefined are automatically coerced to globalThisvariables, which can lead to many unexpected errors. But in strict mode, referencing this value as null or undefined will throw an error.
  • Duplicate property names or parameter values ​​are prohibited. Strict mode throws errors when it detects duplicate-named properties in objects or duplicate-named parameters of functions (e.g. functions foo(val1, val2, val1){}), catching almost certainly bugs in code that would otherwise waste a lot of time tracking down.
  • make eval()more secure. eval()Behaves differently in strict and non-strict mode. Most importantly, in strict mode, eval()variables and functions declared in statements are not created within the containing scope. They are created in non-strict mode in the containing scope, which can also be a common problem with JavaScript.
  • An error is thrown when invalidly used delete. The delete operator (used to remove a property from an object) cannot be used on non-configurable properties of an object . Non-strict mode code will fail silently when attempting to remove a non-configurable property , whereas strict mode will throw an error in this case.

Well, the above is a summary of some questions I want to write to junior Javascript developers.
In the end, I'd like to say that, as with any technology, the better you understand how JavaScript works, the more reliable your code will be, and the more effectively you'll be able to use the power of the language to solve problems. Instead, a lack of proper understanding of JavaScript concepts is the root of many JavaScript problems. Thorough familiarity with the nuances and subtleties of the language is the most effective strategy for increasing your coding productivity.

My blog will be synchronized to the Tencent Cloud developer community, and everyone is invited to join: https://cloud.tencent.com/developer/support-plan?invite_code=1tf7dzx2j95o6

Guess you like

Origin blog.csdn.net/ImagineCode/article/details/132129801