5 practical JavaScript development tips

This article is to share 5 super practical JavaScript development skills!

1.Promise.all()、Promise.allSettled()

We can use Promise, async/await to handle asynchronous requests. When processing asynchronous requests concurrently, Promise.all() and Promise.allSettled() can be used to achieve this.

Promise.all()

The Promise.all() static method takes a Promise iterable as input and returns a Promise. When all input Promises are fulfilled, the returned Promise will also be fulfilled (even if an empty iterable is passed in), and an array containing all fulfilled values ​​is returned. If any of the entered Promises are rejected, the returned Promise will be rejected with the reason for the first rejection.

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));

// 输出结果: [ 555, 'foo', 23 ]

As you can see, when all three Promises are resolved, Promise.all() is resolved and the value is printed. But what if one or more Promises are rejected without being resolved?

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

const allPromises = [promise1, promise2, promise3];

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

// 输出结果: rejected!

Promise.all() is rejected if at least one of its elements is rejected. In the above example, if passed two resolved Promises and one Promise that is rejected immediately, then Promise.all() will be rejected immediately.

Promise.allSettled()

The Promise.allSettled() method was introduced in ES2020. It takes an iterable of Promises as input, and unlike Promise.all() it returns a Promise that is always resolved after all the given Promises are resolved or rejected. The Promise resolves with an array of objects describing the outcome of each Promise.

For the result of each Promise, there are two possible states:

  • fulfilled: A value containing the result.
  • rejected: Contains the reason for the rejection.
const promise1 = Promise.resolve(555);
const promise2 = new Promise(resolve => setTimeout(resolve, 100, 'foo'));
const promise3 = Promise.reject('rejected!');

const allPromises = [promise1, promise2, promise3];

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

// 输出结果:
// [
//   { status: 'fulfilled', value: 555 },
//   { status: 'fulfilled', value: 'foo' },
//   { status: 'rejected', reason: 'rejected!' }
// ]

So how to choose between the two methods? If you want to "fail fast", you should choose Promise.all(). Consider a scenario where you need all requests to succeed, and then define some logic based on this success. In this case, fast fail is acceptable, because after one request fails, the results of other requests are irrelevant, and you don't want to waste resources on the remaining requests.

In other cases, it is desirable that all requests are either rejected or resolved. Promise.allSettled() is the right choice if fetched data is used for subsequent tasks, or if you want to display and access per-request error messages.

2. Null coalescing operator: ??

The null coalescing operator returns the right operand if the left operand is null or undefined, otherwise returns the left operand. It's a neat syntax for getting the value of the first "defined" of two variables.

For example, the result of x ?? y is:

  • Returns x if x is not null or undefined.
  • If x is null or undefined, returns y.

So, x ?? y can be written as:

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

A common use of ?? is to provide a default value. For example, in the following example, when the value of name is not null/undefined, its value is displayed, otherwise "Unknown" is displayed:

const name = someValue ?? "Unknown";
console.log(name);

If someValue is not null or undefined, then the value of name will be someValue; if someValue is null or undefined, then the value of name will be "Unknown".

?? vs ||

The logical AND operator (||) can be used in the same way as the null coalescing operator (??). You can replace ?? with || and still get the same result, for example:

let name;
console.log(name ?? "Unknown"); // 输出结果: Unknown
console.log(name || "Unknown"); // 输出结果: Unknown

The difference between them is: || returns the first true value. ?? Returns the first defined value (defined = not null or undefined). That is, the || operator does not distinguish between false, 0, "" and null/undefined, which are all false values. If any of these are the first argument to ||, then the result will be the second argument. For example:

let grade = 0;
console.log(grade || 100); // 输出结果: 100
console.log(grade ?? 100); // 输出结果: 0

grade || 100 checks whether grade is a false value, while its value is 0, which is indeed a false value. So the result of || is the second argument, which is 100. And grade ?? 100 checks to see if grade is null or undefined, which it isn't, so the result of grade is 0.

So how to choose between the two methods?

  • Scenarios for using the null coalescing operator (??):

Provide default values ​​for variables: When a variable may be null or undefined, you can use the null coalescing operator to provide a default value for it. For example: const name = inputName ?? "Unknown";

Handling potentially missing properties: When accessing object properties, if the property may exist but has a value of null or undefined, the null coalescing operator can be used to provide a default value. For example: const address = user.address ?? "Unknown";

Avoid false value cases: Null coalescing operator can be used when we only want to process explicitly defined values ​​and avoid processing false values ​​like false, 0, empty string, etc. For example: const value = userInputValue?? 0;

  • Use scenarios of the logical OR operator (||):

Provide alternative values: When we need to select a valid value from multiple options, we can use the logical OR operator. For example: const result = value1 || value2 || value3;

Judgment Condition: When we need to check whether any one of multiple conditions is true, we can use the logical OR operator. For example: if (condition1 || condition2) { // perform operation 

3.this

"this" is an often misunderstood concept in JavaScript. To use "this" correctly in JavaScript, you need to really understand how it works, because it has some differences from other programming languages.

Here is an example of a common error when using "this":

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

obj.printHelloWorld();
// 输出结果: Hello World!

obj.printHelloWorldAfter1Sec();
// 输出结果: undefined

The first result prints "Hello World!" because this.helloWorld correctly points to the object's name property. The second result is undefined because this has lost its reference to the object property. This is because what this refers to depends on the object that called the function it is placed on. There is a this variable in every function, but the object it points to is determined by the object that called it.

In obj.printHelloWorld(), this directly points to obj. In obj.printHelloWorldAfter1Sec(), this directly points to obj. However, in the callback function of setTimeout, this does not point to any object, because there is no object to call it. The default object (usually window) is used. name does not exist on window, so undefined is returned.

To use this correctly, you need to know the object it is bound to when the function is called. If you want to access object properties in the callback function, you can use arrow functions or explicitly bind the correct this value through the bind() method to avoid errors.

How to fix this problem? The best way to keep this reference in setTimeout is to use arrow functions. Unlike normal functions, arrow functions don't create their own this.

Therefore, the following code will keep a reference to this:

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

obj.printHelloWorld();
// 输出结果: Hello World!

obj.printHelloWorldAfter1Sec();
// 输出结果: Hello World!

Instead of using arrow functions, there are other ways to solve this problem.

  • Use the bind() method: The bind() method creates a new function and returns after specifying its this value. 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 two methods allow specifying a specific this value to call the function. The difference between them is that the call() method accepts an array of values ​​as an argument whereas the apply() method accepts an array as an argument.
  • Using the self variable: This was a common approach before the introduction of arrow functions. 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.

In general, each method has its advantages and disadvantages, and the choice of which method to use depends on the specific usage scenario. Arrow functions are recommended by default for most cases.

4. Memory usage

Sometimes the memory usage of the application can be very bad, see the following example:

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 to each entity, depending on its type:

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

console.log(tooManyTimesMappedArr);
// 输出结果:
// [
//   { 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' }
// ]

As you can see, by using map, it is possible to do a simple transformation and use it multiple times. For a small array the memory consumption is insignificant, but for larger arrays you will definitely find a significant impact of memory.

So, what are the better solutions in this case?

First, you need to understand that when dealing with large arrays, space complexity is exceeded. Then, think about how to reduce memory consumption. In this example, there are a few good options:

1. Use map in a chain to avoid multiple clones:

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);
// 输出结果:
// [
//   { 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. A better way is to reduce the number of map and clone operations:

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

console.log(mappedArr);
// 输出结果:
// [
//   { 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. Use Map or Object instead of switch-case

Consider the following example:

function findCities(country) {
  switch (country) {
    case 'Russia':
      return ['Moscow', 'Saint Petersburg'];
    case 'Mexico':
      return ['Cancun', 'Mexico City'];
    case 'Germany':
      return ['Munich', 'Berlin'];
    default:
      return [];
  }
}

console.log(findCities(null));      // 输出结果: []
console.log(findCities('Germany')); // 输出结果: ['Munich', 'Berlin']

The code above seems to be fine, but the same result can be achieved with a cleaner syntax using object literals:

const citiesCountry = {
  Russia: ['Moscow', 'Saint Petersburg'],
  Mexico: ['Cancun', 'Mexico City'],
  Germany: ['Munich', 'Berlin']
};

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

console.log(findCities(null));      // 输出结果: []
console.log(findCities('Germany')); // 输出结果: ['Munich', 'Berlin']

Map is an object type introduced in ES6 that allows storing key-value pairs, you can also use Map to achieve the same result:

const citiesCountry = new Map()
  .set('Russia', ['Moscow', 'Saint Petersburg'])
  .set('Mexico', ['Cancun', 'Mexico City'])
  .set('Germany', ['Munich', 'Berlin']);

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

console.log(findCities(null));      // 输出结果: []
console.log(findCities('Germany')); // 输出结果: ['Munich', 'Berlin']

So should we stop using switch statements? no. Using object literals or Maps where possible can improve code level and make it more elegant.

The main differences between Map and object literals are as follows:

  • Keys:  In Maps, keys can be of any data type (including objects and primitive values). In object literals, keys must be strings or symbols.
  • Iteration:  In Map, it can be iterated using for...of loop or forEach() method. In object literals, you need to use Object.keys(), Object.values() or Object.entries() to iterate.
  • Performance:  In general, Maps perform better than Object literals when dealing with large datasets or frequent additions/deletions. For small datasets or infrequent operations, the performance difference is negligible. The choice of which data structure to use depends on the specific use case. 

Guess you like

Origin blog.csdn.net/wangonik_l/article/details/132409140