Maison > Article > interface Web > Erreurs JavaScript courantes commises par les développeurs
Avez-vous déjà été dans une situation où vous créez une application Web fonctionnelle à l'aide de JavaScript, et tout à coup, votre build parfaite devient un cauchemar noyé au milieu d'épaisses lignes rouges (erreurs) ? La frustration s'installe et vous vous inquiétez, vous remettez en question vos compétences et vous demandez : « Qu'est-ce qui n'a pas fonctionné ? »
Curieusement, dans la plupart des scénarios, la cause de la rupture du code et des erreurs ne sont pas des bugs complexes mais plutôt une erreur courante cachée dans les fondamentaux de votre code. Ce guide sera la boussole qui vous guidera dans votre parcours de codage dans de telles situations.
Dans cet article, vous apprendrez comment éviter ces erreurs courantes. Ce guide fournira des explications intéressantes et des solutions pratiques où vous verrez les erreurs et comment les corriger.
Sans plus tarder, passons directement au sujet.
En JavaScript, vous pouvez déclarer des variables à l'aide de mots-clés tels que let, var ou const. Bien qu'il soit courant pour les développeurs de percevoir var et let comme interchangeables en raison de leur fonctionnalité de mutabilité partagée (permettant de modifier ou de muter les variables déclarées avec eux), il existe une distinction cruciale concernant leur portée. Il est essentiel de reconnaître que ces mots-clés diffèrent considérablement dans la façon dont ils définissent la portée d'une variable.
JavaScript divise la portée en portées globales, de fonctions et de blocs. Dans ce segment, vous n'avez qu'à vous concentrer sur deux types de scopes : fonction et bloc.
Portée de la fonction : Les variables déclarées dans une fonction ne sont accessibles qu'à l'intérieur de cette fonction et non à l'extérieur. Cette technique encapsule et empêche toute modification accidentelle provenant d'autres parties de votre code.
Portée du bloc : introduite dans ES6 (ECMAScript 2015), la portée du bloc vous permet de déclarer des variables avec les mots-clés let et const dans des blocs de code spécifiques définis par des accolades, tels que des instructions if, des boucles et fonctions de flèche. Cela permet un contrôle encore plus précis de l'accessibilité variable et aide à prévenir les effets secondaires indésirables.
Au vu de la définition de la portée du bloc, vous avez peut-être remarqué l'absence du mot-clé var. Était-ce un oubli ? Pas du tout! Les variables définies à l'aide du mot-clé var sont de portée fonction, tandis que celles déclarées avec let ou const sont de portée bloc. Les extraits de code suivants élucideront davantage ces concepts.
Commençons par traduire la définition de la portée de la fonction en code :
function testVar() { var test = "new variable"; } console.log(test); // This will return an error: Uncaught ReferenceError: test is not defined.
Le code ci-dessus confirme que les variables déclarées avec le mot-clé var sont de portée fonctionnelle. Lorsque vous tentez d'accéder à la variable de test à l'aide de l'instruction console.log() en dehors de la fonction où elle a été définie, vous obtenez une erreur : "Uncaught ReferenceError : le test n'est pas défini."
Dans ce scénario, la variable est limitée localement à la fonction où elle a été définie puis détruite. Par conséquent, en dehors de la fonction, l'instruction console.log() provoque une erreur car la variable est supprimée de la pile mémoire lorsqu'elle est détruite.
Cependant, contrairement à la tentative précédente, l'accès à la variable de test à l'aide de l'instruction console.log() dans la fonction de définition récupère la valeur de la variable sans rencontrer d'erreur de référence.
function testVar() { var test = "new variable"; console.log(test); // This returns - new variable } testVar();
Le code ci-dessous vous aidera à comprendre comment ces variables fonctionnent par rapport à leur type de portée.
function variableScopes() { // testing var scope var varTest = 1; if (true) { var varTest = 2; console.log("Inside this if block, var yields this:", varTest); } console.log("Outside the if block, var yields this:", varTest); // testing let scope let letTest = 1; if (true) { let letTest = 2; console.log("Inside this if block, let yields this:", letTest); } console.log("Outside the if block, let yields this:", letTest); // testing const scope const constTest = 1; if (true) { const constTest = 2; console.log("Inside this if block, const yields this:", constTest); } console.log("Outside the if block, const yields this:", constTest); } variableScopes(); // The code result: // Inside this if block, var yields this: 2 // Outside the if block, var yields this: 2 // Inside this if block, let yields this: 2 // Outside the if block, let yields this: 1 // Inside this if block, const yields this: 2 // Outside the if block, const yields this: 1
Déductions du code ci-dessus :
var : le mot-clé Thevar est limité à la fonction, ce qui signifie que les variables déclarées avec var sont accessibles dans toute la fonction. Le code fourni déclare la variable varTest deux fois à l'intérieur et à l'extérieur de l'instruction if. Cependant, en raison de la portée de la fonction var, la variable est essentiellement redéclarée dans la même portée, ce qui entraîne un comportement inattendu. Par conséquent, lorsque la valeur de varTest est modifiée à l'intérieur du bloc if, cela affecte la variable déclarée à l'extérieur du bloc, conduisant les deux instructions console.log à afficher la valeur 2.
let : contrairement à var, le mot-clé let a une portée de bloc, ce qui signifie que les variables déclarées avec let ne sont accessibles que dans le bloc défini. Dans le code, la variable letTest est déclarée à l'intérieur et à l'extérieur de l'instruction if à l'aide du mot-clé let. Comme prévu avec la portée du bloc, la modification de la valeur de letTest à l'intérieur du bloc if n'affecte que la variable dans ce bloc. Par conséquent, le premier console.log à l'intérieur du bloc if affiche une valeur de 2, tandis que celui à l'extérieur du bloc conserve la valeur initiale de 1.
const: Similar to the let keyword, variables declared with const are also block-scoped. The code declares the variable constTest as a constant inside and outside the if statement. Despite being constants, the block scope allows re-declaration within different blocks. However, reassigning a value to a constant is not allowed. Thus, modifying the value of constTest inside the if block affects only the variable within that block, while the value outside the block remains unchanged.
Mastering the use of equality operators is crucial for JavaScript developers. Unlike other languages that rely on a single type, JavaScript offers two: the strict equality operator and the loose equality operator. Understanding the nuances between these operators is essential for writing robust and error-free JavaScript code.
The loose equality performs type coercion before comparison. This operator tends to convert the operands to the same type before evaluating the equality. While this may seem convenient, it often leads to unexpected results, especially when comparing different data types.
The loose equality operator in JavaScript follows several basic rules to determine when to perform type coercion:
If the operands have the same type, In this case, where both have the same type, no coercion is necessary, and the equality comparison gets done.
If one operand is null and the other is undefined, JavaScript treats null and undefined as equal when using loose equality. Also, null and undefined values can't undergo type coercion, so they are equal.
If one operand is a number and the other is a string, JavaScript will attempt to convert the string operand to a number before comparing the values.
If one operand is a boolean, If one operand is a boolean, JavaScript will attempt to convert that operand to the numeric value. The numeric values: the value of false is converted to 0, whereas the value of true is converted to 1.
If one operand is an object and the other is not, JavaScript will attempt to convert the object operand to a primitive value (using the valueOf and toString methods) before comparing.
If both operands are objects, JavaScript compares their references to determine whether they are the same object. Note that the references are compared, not their values. Two objects are only equal if they reference the same object in memory.
If either operand is NaN, the loose equality operator returns false. Note that even if both operands are NaN, you still get false because, by default, NaN is not equal to NaN.
Strict equality compares both values and types strictly without performing any type coercion. It demands that both operands have the same value and data type for equality to be true. This approach offers clarity and predictability, eliminating the pitfalls of implicit type conversion.
It's of utmost importance to be aware of the potential issues and unexpected outcomes that can arise when using loose equality. To steer clear of these, it is strongly recommended that you always use strict equality when comparing operands. This practice will help you maintain data type integrity throughout your codebase, ensuring a more robust and reliable application.
Here is a code snippet for testing different scenarios with their respective results:
// These snippets showcase how JavaScript's loose and //strict equality operators behave in various scenarios. // Testing loose equality const num1 = 5; const num2 = "5"; console.log(num1 == num2); // Testing strict equality: const num3 = 5; const num4 = "5"; console.log(num3 === num4); // false // More examples of loose equality const value1 = 0; const value2 = false; console.log(value1 == value2); // true // More examples of strict equality: const value3 = 0; const value4 = false; console.log(value3 === value4); // false // Array comparison with loose equality: const arr1 = [1, 2, 3]; const arr2 = "1,2,3"; console.log(arr1 == arr2); // true // Array comparison with strict equality: const arr3 = [1, 2, 3]; const arr4 = "1,2,3"; console.log(arr3 === arr4); // false
JavaScript developers are often fond of adding an else block immediately after an if block, even when the logic inside the else block is essentially the default behaviour or can be handled without the else block. This leads to unnecessary code complexity, which can be simplified by restructuring the code.
An else block is executed when the condition of the preceding if statement is evaluated as false. However, in situations where the else block contains code that can be executed without explicitly checking the condition, using the else block becomes redundant and can be avoided.
Code example:
const isEven = (num) => { if (num % 2 === 0) { return true; } else { return false; } } isEven(4); // true isEven(3); // false
In this code, the function isEven takes a number as input and checks if it's even. If the number is divisible by two without a remainder, it returns true, indicating that the number is even. Otherwise, it returns false, indicating that the number is odd. However, using the else block is unnecessary because if the condition in the if statement is not met (i.e., the number is not even), the function will naturally reach the next line after the if block without needing an else statement.
Now, without the else statement:
const isEven = (num) => { if (num % 2 === 0) { return true; } return false; } isEven(4); // true isEven(3); // false
This code is a more concise version of the first code. It checks if the number is even using the if statement; if it is, it returns true. However, if the number is not even, it will return false. There's no need for an else block because the return false statement will only execute if the condition for the if statement is false.
You can even simplify it further by using an arrow function like this:
const isEven = (num) => { return num % 2 === 0; }
This code further simplifies the logic by directly returning the result of the expression num % 2 === 0. If this expression is evaluated as true, the function returns true, indicating that the number is even. Otherwise, it returns false.
A developer must understand the difference between mutable and immutable types and how they work under the hood. This segment will take objects as the case study to deeply understand how mutable types work and the mistakes usually made when working with them.
Consider the code snippet below to understand how immutable types work in JavaScript:
let x = 2; let y = x; // y becomes the copy of the value in variable x which is 2 x += 2; y += 1; console.log(x, y); //This will return 4, 3
In this code sample, we define a variable x with a value of 2. The variable x holds an immutable type, precisely a number. Once you define an immutable type, you can't change it directly.
The line "let y = x" creates a copy of the variable x value and assigns it to y. It's crucial to understand that y is independent of x; any operations performed on x will not affect y, and vice versa. This concept is fundamental to understanding immutable types.
Mutable types, such as objects and arrays in JavaScript, behave differently from immutable types. When you assign one variable to another with mutable types, they point to the same underlying data in memory. This means that modifying one variable will directly affect the other, as they share a reference to the same data.
Let's consider a simple array code snippet to illustrate this concept:
let x = []; let y = x; x.push(1); y.push(2); console.log(x); // output [1, 2] console.log(y); // output [1, 2]
In the code above, variables x and y are arrays initially empty. When we push elements into x and y, we observe that both arrays end up with the same elements - [1, 2]. Unlike immutable types, this behaviour is because mutable types store a reference to the object rather than the actual value.
Here's a breakdown of what happens in the code snippet:
A common mistake occurs when developers mix mutable and immutable types, mainly when dealing with nested objects. Let’s consider the code snippet below:
const developer = { name: "John Doe", age: 25, core: { programmingLanguage: "JavaScript", framework: "React", role: "Frontend Developer", }, }; // creating a similar developer but with a different name and language const developer2 = developer; developer2.name = "Jane Doe"; developer2.core.programmingLanguage = "TypeScript"; console.log("developer", developer); // {name: 'Jane Doe', age: 25, core: {programmingLanguage: 'TypeScript', framework: 'React', role: 'Frontend Developer"}} console.log("developer2", developer2); // {name: 'Jane Doe', age: 25, core: {programmingLanguage: 'TypeScript', framework: 'React', role: 'Frontend Developer"}}
When you run the code above, you will notice that developer2, which copies the property of the developer object, modified the developer object, automatically making the two objects identical. Now, I believe you understand why! It is because they are both pointing to the same reference in memory. The question is, "How can you have the two objects without them modifying each other?"
When dealing with objects in JavaScript, a common approach for creating a copy is to use the spread operator (...). However, it's important to note that this method, known as shallow copy, only addresses part of the cloning process.
Consider the use of the spread operator in creating a shallow copy:
const developer2 = { ...developer };
While this method prevents direct modifications to the global object developer, it falls short when dealing with nested objects. Changes to nested properties in the copied object developer2 can still impact the original object developer. For example:
developer2.core.programmingLanguage = "TypeScript";
This modification affects the programmingLanguage value in the nested core object of both developer and developer2.
The reason behind this behaviour lies in how the spread operator operates. It copies the object's properties, but when encountering nested objects, it copies their references rather than their values. As a result, changes to nested objects in the copied version get reflected in the original object due to shared references.
To overcome this limitation and ensure a complete separation between objects, developers turn to a concept known as deep cloning. Deep cloning involves creating a new object with independent values, including nested objects.
In JavaScript, a standard method for deep cloning is using JSON.parse() and JSON.stringify(). To see how this works, replace this line of code:
const developer2 = developer;
With this:
const developer2 = JSON.parse(JSON.stringify(developer));
This new line of code creates a deep clone of the developer object, ensuring that modifications to developer2 do not affect the developer. The JSON.stringify() method converts the object into a string (an immutable type), and then JSON.parse() converts it back into an object, effectively creating a new object with its separate memory reference. Note that there are other methods for performing deep cloning.
The code using deep cloning:
const developer = { name: "John Doe", age: 25, core: { programmingLanguage: "JavaScript", framework: "React", role: "Frontend Developer", }, }; // creating a similar developer but with a different name and language const developer2 = JSON.parse(JSON.stringify(developer)); developer2.name = "Jane Doe"; developer2.core.programmingLanguage = "TypeScript"; console.log("developer", developer); // {name: 'John Doe', age: 25, core: {programmingLanguage: 'JavaScript', framework: 'React', role: 'Frontend Developer"}} console.log("developer2", developer2); // {name: 'Jane Doe', age: 25, core: {programmingLanguage: 'TypeScript', framework: 'React', role: 'Frontend Developer"}}
One common mistake JavaScript developers encounter is not fully grasping the behaviour of the this keyword within arrow functions compared to regular functions. This distinction is crucial as it affects how this is scoped and accessed within different function types.
Sample Code:
function createProgrammingLanguage(name, author) { return { name, author, useRegularFunction: function () { console.log(`${this.name} was created by ${this.author}`); }, useArrowFunction: () => { console.log(`${this.name} was created by ${this.author}`); }, }; } const javascript = createProgrammingLanguage( "JavaScript", "Brendan Eich", ); javascript.useArrowFunction(); javascript.useRegularFunction();
Run the code above in a code editor. If you use an online code editor like CodeSandbox, you will notice that the editor throws an error - "Cannot read properties of undefined (reading 'type')." It points out that the issue is from the useArrowFunction(), but if you are using an IDE like Visual Studio Code or Sublime Text then you would notice that calling the useArrowFunction() method results in this: "JavaScript was created by Brendan Eich" while calling the useRegularFunction() results in this: "undefined was created by undefined".
The error encountered when running the useArrowFunction method occurs because arrow functions do not have this context; they inherit it from the surrounding lexical scope where they are defined. In this case, since useArrowFunction is defined within createProgrammingLanguage, it refers to the global context (usually a window object in a browser environment) where name, and author are undefined.
On the other hand, useRegularFunction is a regular function defined inside an object literal. Regular functions have their own this context, which is determined by how they are called. In this context, this correctly refers to the object returned by createProgrammingLanguage(), allowing useRegularFunction() to access name and author properties without issues.
This solution demonstrates how to correctly use this within an arrow function by leveraging lexical scoping.
function createProgrammingLanguage(name, author) { return { name, author, overallFunction: function () { console.log( `Regular function: ${this.name} was created by ${this.author}`, ); // Arrow function inherits this from the parent function const arrowFunction = () => { console.log( `Arrow function: ${this.name} was created by ${this.author}`, ); }; arrowFunction(); }, }; } const javascript = createProgrammingLanguage("JavaScript", "Brendan Eich"); javascript.overallFunction(); // Result // Regular function: JavaScript was created by Brendan Eich // Arrow function: JavaScript was created by Brendan Eich
Regular Function Context: Inside the overallFunction, this correctly refers to the object returned by createProgrammingLanguage(). Therefore, the values are retrieved when accessing this.name and this.author.
Arrow Function Context: The arrowFunction is defined within overallFunction. Arrow functions inherit this from their surrounding lexical scope, the overallFunction. This inheritance ensures that this within the arrow function refers to the same object as this in the parent regular function. As a result, the arrow function correctly accesses this.name and this.author, producing the desired output without encountering the "Cannot read properties of undefined" error.
By understanding arrow functions' lexical scoping behaviour and leveraging this behaviour to inherit this from the parent function's context, you ensure that arrow functions can access object properties and methods within the appropriate context, resolving issues that result in having the this references being undefined.
Memory leaks in JavaScript can be subtle and insidious, causing problems on devices with limited memory or in frequently called functions. In JavaScript, memory leaks happen when data is held in memory even though it's no longer needed. The primary cause of memory leaks in JavaScript is often unwanted references. Let's explore some common scenarios, how they lead to memory leaks and the solutions.
function defineLanaguage() { language = "JavaScript"; // Accidental global variable }
In this code, language becomes a property of the window object ("window.language = 'JavaScript'"). To prevent this and ensure language is scoped correctly, use var, let, or const:
function defineLanaguage() { let language = "JavaScript"; // Scoped variable }
Using a scoped variable ensures that language goes out of scope at the end of the function's execution, preventing memory leaks.
let language = "JavaScript"; setInterval(() => { console.log(language); }, 100);
In this code, the interval timer keeps a reference to the language variable, preventing it from being garbage collected even when not needed. To avoid this, clear the interval timer when it's no longer required:
let language = "JavaScript"; let intervalId = setInterval(() => { console.log(language); }, 100); // Clear the interval when no longer needed clearInterval(intervalId);
Clearing the interval ensures the handler function and its references are cleaned up properly.
let outer = function () { let language = "JavaScript"; return function () { return language; }; };
This closure retains a reference to the language variable, preventing it from being collected as garbage. To avoid this, ensure that closures release unnecessary references:
let outer = function () { let language = "JavaScript"; return function () { return language; }; }; // Clear the outer function reference when no longer needed outer = null;
Clearing the reference to the outer function allows the language variable to be garbage collected when it's no longer needed.
By addressing these common scenarios of memory leaks in JavaScript, developers can improve the efficiency and stability of their code. To prevent memory leaks, always scope variables correctly, manage interval timers responsibly, and release unnecessary closures. You can research and see some other instances that lead to memory leaks, but the ones mentioned here are common.
One common mistake in JavaScript is improperly using a variable as a key when defining an object literal. This mistake can lead to unexpected behaviour or incorrect object structures. Let's delve into this issue, understand why it occurs, and explore the correct approach to defining object keys dynamically.
In order to understand the mistake, consider the following code snippet:
var type = true; var language = null; if (type) { language = "TypeScript"; } else { language = "JavaScript"; } var obj = { language: "programming language", }; console.log(obj); // Output - { language: "programming language" }
In the code above, the mistake is using the variable language directly as the object (obj) key. Despite language being dynamically assigned a value based on the type condition, it is interpreted as a literal key language rather than using its value as the key.
Expected Result vs Actual Result
JavaScript provides a syntax using square brackets ([ ]) to use a variable as a dynamic key in an object. This allows for the evaluation of the variable's value as the key.
var type = true; var language = null; if (type) { language = "TypeScript"; } else { language = "JavaScript"; } var obj = { [language]: "programming language", }; console.log(obj); // Output - { TypeScript: "programming language" }
Through this exploration of common JavaScript mistakes, you've gained valuable insights into how to write cleaner, more efficient code.
Keep growing, learning, and building!
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!