5 Essential Points You Should Be Familiar With as a JavaScript Developer.
JavaScript logo

5 Essential Points You Should Be Familiar With as a JavaScript Developer.




Introduction


In this article, we will be discussing some JavaScript topics, that in my opinion, every developer should know. At times, we may think that we can enhance the quality of some code segments when working with JavaScript. Based on my own experience, I considered ways to help some developers avoid mistakes and make good decisions about their code.

Let’s dive into our topics:




#1 - Promise.all() vs Promise.allSettled()


Handling promises is a major part of writing JavaScript. There are many ways to handle it, but it’s important to think about what works best for you.


Promise.all()


This method takes an iterable of promises as its input parameter and returns a single promise. The result of each input promise will be resolved into an array of the entire results. Let’s have a look at the example below:



#1.


const promise1 = Promise.resolve(555);
const promise2 = new Promise(resolve => setTimeout(resolve, 100, 'foo'));
const promise3 = 23;

const allPromises = [promise1, promise2, promise3];
Promise.all(allPromises).then(values => console.log(values));

// Output: [ 555, 'foo', 23 ]        


As you can see, when all three promises get resolved, Promise.all() resolves and the values will be printed. But wait, what if one promise (or more) is not resolved and gets rejected?



#2.


const promise1 = Promise.resolve(555);
const promise2 = new Promise(resolve => setTimeout(resolve, 100, 'foo'));
const promise3 = Promise.reject('I just got rejected!');

const allPromises = [promise1, promise2, promise3];

Promise.all(allPromises)
  .then(values => console.log(values))
  .catch(err => console.error(err));

// Output: I just got rejected!        


Promise.all() is rejected if at least one of the elements is rejected. In the above example, if we pass 2 promises that resolve and one promise that rejects immediately, then Promise.all() will reject immediately.


Promise.allSettled()


This method was introduced in ES2020. Promise.allSettled() takes an iterable of promises as its input parameter, but in contrast to Promise.all() , it returns a promise that always resolves after all the given promises have either been fulfilled or rejected. The promise is resolved with an array of objects that describe each promise’s outcome.


For each promise outcome, we get either:

  • fulfilled status, with the value of the result.
  • rejected status, with a reason of the rejection.


Let’s take a closer look:


#3.


const promise1 = Promise.resolve(555);
const promise2 = new Promise(resolve => setTimeout(resolve, 100, 'foo'));
const promise3 = Promise.reject('I just got rejected!');

const allPromises = [promise1, promise2, promise3];

Promise.allSettled(allPromises)
  .then(values => console.log(values))

// Output:
// [
//   { status: 'fulfilled', value: 555 },
//   { status: 'fulfilled', value: 'foo' },
//   { status: 'rejected', reason: 'I just got rejected!' }
// ]        


Which one should I choose?


If you want to “fail fast”, you should probably choose Promise.all() .

Consider a scenario where you need all requests to be fulfilled, and define some logic based on that success. In such a case, failing fast is fine, as after you fail on one of the requests, the other calls are not relevant anymore. You don’t want to waste resources on the remaining calls.

However, in other cases, you want all calls to either be rejected or fulfilled. If the fetched data is used for a separate subsequent task, or you want to show and access error information about each call, Promise.allSettled() is the right choice.




#2 - Nullish Coalescing Operator ‘??’


The nullish coalescing operator is written as two question marks ?? . This operator returns the right-hand side operand when its left-hand side operand is null or undefined, or otherwise returns its left-hand side operand.


It’s easy to understand using a quick example. The result of x ?? y is:

  • if x isn’t null or undefined, then x .
  • if x is null or undefined, then y .


The nullish coalescing operator isn’t something we see a lot, especially from new Javascript developers. It’s just a nice syntax to get the first “defined” value of the two variables.


You can actually write x ?? y like this:



#4.


result = (x !== null && x !== undefined) ? x : y;        


Now it should be clear what ?? does.


The common use case for ?? is to provide a default value. For example, here we show name when its value isn’t null/undefined, otherwise Unknown:



#5.


let name;
alert(name ?? "Unknown"); // Output: Unknown (name is undefined)        


Here’s an example with name being assigned with the left operand:



#6.


let name = "Denis";
alert(name ?? "Unknown"); // Denis (name is neither null nor undefined)        




Comparison with OR “||” operator


The OR || operator can be used in the same way as ?? . You can replace ?? with || and still get the same result, for example:



#7.


let name;
alert(name ?? "Unknown"); // Output: Unknown
alert(name || "Unknown"); // Output: Unknown        


The OR || operator has been around since the beginning of JavaScript, so developers have been using it for these purposes for a long time.

The nullish coalescing operator ?? was added to JavaScript only recently (ES2020), as people weren’t quite happy with || .


The important difference between them is:

  • || returns the first truthy value.
  • ?? returns the first defined value (defined = not null or undefined).


In other words, || doesn’t distinguish between false, 0, an empty string "" and null/undefined. They are all the same – falsy values. If any of these is the first argument of || , then you’ll get the second argument as the result.

For example:



#8.


let grade = 0;
alert(grade || 100); // Output: 100
alert(grade ?? 100); // Output: 0        


The grade || 100 checks grade for being a falsy value, and it’s 0 , which is indeed falsy. So the result of || is the second argument, 100 .

The grade ?? 100 checks grade for being null or undefined, and it’s not, so the result of grade stays 0.




#3 - Wrong References To “this”


this is a commonly misunderstood concept in JavaScript. To use this in JavaScript, you really need to understand how it works because it operates a little differently compared to other languages.


Here’s an example of a common mistake when using this :



#9.


const obj = {
  helloWorld: "Hello World!",
  printHelloWorld: function () {
    console.log(this.helloWorld);
  },
  printHelloWorldAfter1Sec: function () {
    setTimeout(function () {
      console.log(this.helloWorld);
    }, 1000);
  },
};

obj.printHelloWorld();
// Output: Hello World!

obj.printHelloWorldAfter1Sec();
// Output: undefined        


The first result consoles Hello World! because this.helloWorld correctly points to the object's name property. The second result is undefined because this has lost the reference to the object's properties.

This is because this depends on the object calling the function in which it lives in. There is a this variable in every function but the object it points to is determined by the object calling it.

The this in obj.printHelloWorld() points directly to obj.

The this in obj.printHelloWorldAfter1Sec() points directly to obj.

But the this in the callback function of setTimeout does not point to any object because no object is called it. The default object (which is window) is used. helloWorld does not exist on window, resulting in undefined.


How can we fix it?


The best way to retain the reference to this in setTimeout is to use arrow functions (which were introduced in ES6). Unlike normal functions, arrow functions do not create their own this.

So, the following will retain its reference to this:



#10.


const obj = {
  helloWorld: "Hello World!",
  printHelloWorld: function () {
    console.log(this.helloWorld);
  },
  printHelloWorldAfter1Sec: function () {
    setTimeout(() => {
      console.log(this.helloWorld);
    }, 1000);
  },
};

obj.printHelloWorld();
// Output: Hello World!

obj.printHelloWorldAfter1Sec();
// Output: Hello World!        


Instead of using the arrow function, you can use other methods to reach a solution. I will explain them briefly:

  • Using the bind() method: The bind() method creates a new function with a specified this value and returns it. You can use it to bind a function to a specific object, ensuring that this always refers to that object.
  • Using the call() and apply() methods: These methods allow you to call a function with a specific this value. The difference between them is that call() takes arguments as a list of values, while apply() taking arguments as an array.
  • Using the self variable: This is a common approach that was used before arrow functions were introduced. The idea is to store a reference to this in a variable and use that variable inside the function. Note that this approach may not work well with nested functions.


Overall, each of these approaches has its advantages and disadvantages, and the choice of which one to use depends on the specific use case. For most cases, as a default selection, I still recommend using the arrow function.




#4 - Bad Memory Usage


This problem is easy to recognize at runtime when you monitor your server instances and see the average memory consumption increase.

The first thing someone might think of doing is increasing the default memory limit of Node JS.

But you might want to ask yourself first:


“Why is the memory consumption so high?”


The situation has many cases, and I will cover only one common case, in order to get your attention about this issue.

Let’s have a look at the following example.


For the data below:



#11.


const data = [
  { name: 'Frogi', type: Type.Frog },
  { name: 'Mark', type: Type.Human },
  { name: 'John', type: Type.Human },
  { name: 'Rexi', type: Type.Dog }
];        


We want to add some properties for each entity, depending on his type :



#12.


const mappedArr = data.map((entity) => {
  return {
    ...entity,
    walkingOnTwoLegs: entity.type === Type.Human
  }
});
// ...
// some other code
// ...
const tooManyTimesMappedArr = mappedArr.map((entity) => {
  return {
    ...entity,
    greeting: entity.type === Type.Human ? 'hello' : 'none'
  }
});

console.log(tooManyTimesMappedArr);
// Output:
// [
//   { name: 'Frogi', type: 'frog', walkingOnTwoLegs: false, greeting: 'none' },
//   { name: 'Mark', type: 'human', walkingOnTwoLegs: true, greeting: 'hello' },
//   { name: 'John', type: 'human', walkingOnTwoLegs: true, greeting: 'hello' },
//   { name: 'Rexi', type: 'dog', walkingOnTwoLegs: false, greeting: 'none' }
// ]        


You can see how with a map, we can perform a simple transformation and use it multiple times. For a small array, the memory consumption is insignificant, but for larger arrays, we would definitely find a big memory impact.


So what are the better solutions in that case?


First, you need to understand that you are exceeding your space complexity for big arrays. Afterward, think about how you can reduce the in-memory consumption. In our case, there are several good options:




1. Chain the maps, avoiding multiple clonings:



#13.


const mappedArr = data
  .map((entity) => {
    return {
      ...entity,
      walkingOnTwoLegs: entity.type === Type.Human
    }
  })
  .map((entity) => {
    return {
      ...entity,
      greeting: entity.type === Type.Human ? 'hello' : 'none'
    }
  });

console.log(mappedArr);
// Output:
// [
//   { name: 'Frogi', type: 'frog', walkingOnTwoLegs: false, greeting: 'none' },
//   { name: 'Mark', type: 'human', walkingOnTwoLegs: true, greeting: 'hello' },
//   { name: 'John', type: 'human', walkingOnTwoLegs: true, greeting: 'hello' },
//   { name: 'Rexi', type: 'dog', walkingOnTwoLegs: false, greeting: 'none' }
// ]        




2. Even better would be to reduce the number of map and clone operations:



#14.


const mappedArr = data.map((entity) => 
  entity.type === Type.Human ? {
    ...entity,
    walkingOnTwoLegs: true,
    greeting: 'hello'
  } : {
    ...entity,
    walkingOnTwoLegs: false,
    greeting: 'none'
  }
);

console.log(mappedArr);
// Output:
// [
//   { name: 'Frogi', type: 'frog', walkingOnTwoLegs: false, greeting: 'none' },
//   { name: 'Mark', type: 'human', walkingOnTwoLegs: true, greeting: 'hello' },
//   { name: 'John', type: 'human', walkingOnTwoLegs: true, greeting: 'hello' },
//   { name: 'Rexi', type: 'dog', walkingOnTwoLegs: false, greeting: 'none' }
// ]        




#5 - Favor Map/Object Literal Over Switch-Case


We want to print the cities based on their country.

Let’s have a look at the example below:



#15.


function findCities(country) {
  // Use switch case to find cities by country
  switch (country) {
    case 'USA':
      return ['New York', 'Los Angeles'];
    case 'Mexico':
      return ['Cancun', 'Mexico City'];
    case 'Germany':
      return ['Munich', 'Berlin'];
    default:
      return [];
  }
}

console.log(findCities(null));      // Output: []
console.log(findCities('Germany')); // Output: ['Munich', 'Berlin']        


There seems to be nothing wrong with the above code, but I find it to be in quite “hard-coded” style. The same result can be achieved with the object literal with cleaner syntax:



#16.


// Use object literal to find cities by country
const citiesCountry = {
  USA: ['New York', 'Los Angeles'],
  Mexico: ['Cancun', 'Mexico City'],
  Germany: ['Munich', 'Berlin']
};

function findCities(country) {
  return citiesCountry[country] ?? [];
}

console.log(findCities(null));      // Output: []
console.log(findCities('Germany')); // Output: ['Munich', 'Berlin']        


Map is an object type introduced in ES6, that allows you to store key value pairs. Alternatively, you can use Map to achieve the same result:



#17.


// Use Map to find cities by country
const citiesCountry = new Map()
  .set('USA', ['New York', 'Los Angeles'])
  .set('Mexico', ['Cancun', 'Mexico City'])
  .set('Germany', ['Munich', 'Berlin']);

function findCities(country) {
  return citiesCountry.get(country) ?? [];
}

console.log(findCities(null));      // Output: []
console.log(findCities('Germany')); // Output: ['Munich', 'Berlin']        


Should we stop using the switch statement? I’m not saying that.

Personally, I think using object or Map literals whenever possible, increases the level of your code and make it more elegant.


Main differences between Map and object literal:


Keys:

In a Map, the keys can be of any data type (including objects and primitives). In an object literal, keys must be strings or symbols.


Iteration:

In a Map, you can easily iterate over the entries using the for…of loop or the forEach() method. In an object literal, you need to use Object.keys() , Object.values() , or Object.entries() to iterate over the keys, values, or entries.


Performance:

In general, Map performs better than object literals when it comes to large datasets or frequent additions/removals. However, for small datasets or infrequent operations, the performance difference is negligible.


The type of data structure to use depends on the specific use case, but Map and object literals are both useful data structures.




Conclusion


In this article, I have attempted to incorporate some important matters that should be useful to both junior and experienced JavaScript developers. Obviously, there are many more ideas than the ones listed above. So just ensure you stay up to date with the JavaScript developments tools and best practices.


I hope you have learned something new from this article.

Happy coding!


?? Attention! ??


If you find any errors in the computer code or grammar, broken links while reading this article, please let me know. I apologize in advance for any inconvenience caused.



要查看或添加评论,请登录

Denis Rylikov的更多文章

社区洞察

其他会员也浏览了