10 JavaScript Concepts Every Node.js Developer Should Master

This article is translation

Node.js quickly became the standard for creating web applications and system software thanks to the ability to use JavaScript on the server side. Popular frameworks such as Expressand tools like Webpack contribute to its widespread distribution. Despite the existence of competitors such as Deno And BunNode remains the leading platform for server-side JavaScript development.

The multi-paradigm nature of JavaScript allows for a variety of programming styles, but also carries risks such as issues with scope and object modification. The lack of tail recursion optimization makes large recursive iterations dangerous, and Node's single-threaded architecture requires the use of asynchronous code to improve performance. However, by following key concepts and best practices, developers can write scalable and efficient code in Node.js.

1. Closures

A closure is an internal function in JavaScript that has access to the scope of the outer function even after the outer function has finished executing. Closures make the variables of an internal function private. Functional Programming has become very popular, so closures are an important part of the Node.js developer toolkit. Here's a simple example of a closure in JavaScript:

let count = (function () {
    var _counter = 0;
    return function () {
        return _counter += 1;
    };
})();

count();
count();
count();

// the counter is now 3
  • Variable count assigned to an external function. The outer function executes only once, setting the counter to zero and returning the inner function. Variable _counter accessible only to an internal function, which makes its behavior similar to a private variable.

  • The example here is a higher order function (or metafunction), that is, a function that takes or returns another function. Closures are found in many other applications. Closure occurs whenever you define a function inside another function, and the inner function gets both its own scope and access to the parent function's scope – that is, the inner function “sees” the outer function's variables, but not vice versa.

  • This is also convenient when using functional methods such as map(innerFunction)where the inner function can use variables defined in the outer scope.

2. Prototypes

Every function in JavaScript has a property prototypewhich is used to add methods and properties to objects. This property is not enumerable, but allows developers to add methods to objects through their prototypes. JavaScript only supports inheritance through prototypes. An example of using prototypes to add methods:

function Rectangle(x, y) {
    this.length = x;
    this.breadth = y;
}

Rectangle.prototype.getDimensions = function () {
    return { length: this.length, breadth: this.breadth };
};

Rectangle.prototype.setDimensions = function (len, bred) {
    this.length = len;
    this.breadth = bred;
};

Although modern JavaScript has fairly advanced support for classes, it still uses a prototype system under the hood. This is the source of much of the language's flexibility.

3. Private properties using hash names

The early practice was to prefix a variable name with an underscore to indicate that the variable should be private. However, this was just a convention and not a platform-specific restriction. Modern JavaScript suggests using the hash symbol # to create private members and methods in classes:

class ClassWithPrivate {
  #privateField;
  #privateMethod() { }
}

Private variables, denoted by hash marks, are a relatively new but very useful feature in JavaScript! The latest versions of Node and modern browsers support it, and Chrome's developer tools allow you to directly access private variables for convenience.

4. Private properties using closures

Here's another approach you'll sometimes see to get around the lack of private properties in a JavaScript prototyping system: using closures. Modern JavaScript allows you to define private properties using a hashtag prefix #as shown in the previous example. However, this does not work in the JavaScript prototyping system. However, this is a technique you'll often see in code, and it's important to understand how it works.

Defining private properties using closures allows you to mock private variables. Methods that need access to these private properties must be defined directly on the object. Here is the syntax for creating private properties using closures:

function Rectangle(_length, _breadth) {
  this.getDimensions = function () {
    return { length: _length, breadth: _breadth };
  };

  this.setDimension = function (len, bred) {
    _length = len;
    _breadth = bred;
  };
}

This method allows you to protect data by making it inaccessible outside the function, thus ensuring its “privacy”.

5. Modules

When JavaScript didn't have a module system, developers came up with a clever trick (called the modular pattern) to make something work. As JavaScript has evolved, not one, but two module systems have emerged: CommonJS's include syntax and ES6's require syntax.

Node has traditionally used CommonJS, while browsers use ES6. However, in recent years, new versions of Node also support ES6. The trend now is to use ES6 modules, and someday we will only have one module syntax to use in JavaScript. ES6 looks like this (where we export the default module and then import it):

// Module exported in file1.js…
export default function main() { }

// …module imported in file2.js
import main from "./file1";

You may still come across CommonJS and sometimes you will have to use it to import a module. Here's what it looks like to export and then import a default module using CommonJS:

// module exported in file1.js…
function main() {}
module.exports = main;

// …module imported in file2.js
const main = require('./file1');

6. Error handling

No matter what language or environment you work in, error handling is necessary and unavoidable. Node.js is no exception. There are three main ways to handle errors: blocks try/catchthrowing new errors and handlers on().

Blocks try/catch are a proven tool for trapping errors when something goes wrong:

try {
  someRiskyOperation();
} catch (error) {
  console.error("Something's gone terribly wrong", error);
}

In this case we print the error to the console using console.error. You can choose to throw the error, passing it on to the next handler. Note that this interrupts code execution; that is, the current execution stops and the next error handler on the stack takes control:

try {
  someRiskyOperation();
} catch (error) {
  throw new Error("Someone else deal with this.", error);
}

Modern JavaScript offers quite a lot of useful properties on its objects Errorincluding Error.stack to view the stack trace. In the above example we are setting the properties Error.message And Error.cause using constructor arguments.

Another place where you'll find errors is in asynchronous code blocks, where you handle normal results with .then(). In this case you can use a handler on('error') or event onerrordepending on how the promise returns errors. Sometimes the API returns an error object as a second value along with the normal value. (If you are using await for an asynchronous call, you can wrap it in try/catch to handle any errors.) Here is a simple example of asynchronous error handling:

someAsyncOperation()
    .then(result => {
        // All is well
    })
    .catch(error => {
        // Something’s wrong
        console.error("Problems:", error);
    });

Never miss any mistakes! I won't show it here because someone might copy and paste it. Basically, if you catch an error and then do nothing, your program will silently continue running with no obvious indication that anything went wrong. The logic will be broken and you will be left wondering until you find your catch block, which has nothing in it. (Note that providing a block finally{} without block catch will cause your errors to be missed.)

7. Currying

Currying is a technique that makes functions more flexible. With a curried function, you can pass all the expected arguments and get the result, or you can pass only part of the arguments and get a function that waits for the remaining arguments. Here's a simple example of currying:

var myFirstCurry = function(word) {
  return function(user) {
    return [word, ", ", user].join("");
  };
};

var HelloUser = myFirstCurry("Hello");
console.log(HelloUser("InfoWorld")); // Output: "Hello, InfoWorld"

The original curried function can be called directly by passing each of the parameters in a separate pair of parentheses, one after the other:

var myFirstCurry = function(word) {
  return function(user) {
    return [word, ", ", user].join("");
  };
};

console.log(myFirstCurry("Hey, how are you?")("InfoWorld")); 
// Output: "Hey, how are you?, InfoWorld"

This is an interesting technique that allows you to create function factories, where external functions allow you to partially customize internal ones. For example, you can also use the above curried function like this:

var myFirstCurry = function(word) {
  return function(user) {
    return [word, ", ", user].join("");
  };
};

let greeter = myFirstCurry("Namaste");
console.log(greeter("InfoWorld")); 
// Output: "Namaste, InfoWorld"

In the real world, this idea can be useful when you need to create many functions that vary depending on certain parameters.

8. Call, apply and bind methods

Although we don't use them every day, it is important to understand what the methods are call, apply And bind. These methods provide significant flexibility to the language. Essentially, they allow you to specify what the keyword will resolve to this.

In all three functions, the first argument is always the value this or the context you want to pass to the function.

Of all three methods call is the simplest. This is the same as calling a function specifying its context. Here's an example:

var user = {
    name: "Info World",
    whatIsYourName: function() {
        console.log(this.name);
    }
};

user.whatIsYourName(); // Output: "Info World"

var user2 = {
    name: "Hack Er"
};

user.whatIsYourName.call(user2); // Output: "Hack Er"

note that apply almost the same as call. The only difference is that you pass the arguments as an array rather than individually. Arrays are easier to manipulate in JavaScript, which opens up more possibilities for working with functions. Here is an example of use apply And call:

var user = {
    greet: "Hello!",
    greetUser: function(userName) {
        console.log(this.greet + " " + userName);
    }
};

var greet1 = {
    greet: "Hola"
};

user.greetUser.call(greet1, "InfoWorld"); // Output: "Hola InfoWorld"
user.greetUser.apply(greet1, ["InfoWorld"]); // Output: "Hola InfoWorld"

Method bind allows you to pass arguments to a function without calling it. A new function is returned with the arguments bound, preceding any further arguments. Here's an example:

var user = {
    greet: "Hello!",
    greetUser: function(userName) {
        console.log(this.greet + " " + userName);
    }
};

var greetHola = user.greetUser.bind({ greet: "Hola" });
var greetBonjour = user.greetUser.bind({ greet: "Bonjour" });

greetHola("InfoWorld"); // Output: "Hola InfoWorld"
greetBonjour("InfoWorld"); // Output: "Bonjour InfoWorld"

9. Memoization in JavaScript

Memoization is an optimization technique that speeds up the execution of a function by storing the results of expensive operations and returning cached results when the same sets of input data reappear. JavaScript objects behave like associative arrays, which makes it easier to implement memoization in JavaScript. Here's how to convert a recursive factorial function into a memoized factorial function:

function memoizeFunction(func) {
    var cache = {};
    return function() {
        var key = arguments[0];
        if (cache[key]) {
            return cache[key];
        } else {
            var val = func.apply(this, arguments);
            cache[key] = val;
            return val;
        }
    };
}

var fibonacci = memoizeFunction(function(n) {
    return (n === 0 || n === 1) ? n : fibonacci(n - 1) + fibonacci(n - 2);
});

10. IIFE (Immediately Invoked Function Expression)

An immediately invoked function expression (IIFE) is a function that is executed immediately after it is created. It does not involve events or asynchronous execution. You can define IIFE like this:

(function() {
    // all your code here
    // ...
})();

First pair of parentheses function(){...} converts the code inside the parentheses into an expression. The second pair of parentheses calls the function derived from the expression. IIFE can also be described as a self-invoking anonymous function. Most often it is used to limit the scope of a variable created using varor to encapsulate the context to avoid name conflicts.

There are also situations where you need to call a function using awaitbut you are not inside an asynchronous function block. This happens in files that you want to make executable directly, as well as imported as a module. You can wrap such a function call in an IIFE block like this:

(async function() {
    await callAsyncFunction();
})();

11. Useful functions for working with arguments

Although JavaScript does not support method overloading (since functions can take an arbitrary number of arguments), it has several powerful features for working with arguments.

First, you can define a function or method with default values:

function greet(name="Guest") {
    console.log(`Hello, ${name}!`);
}

greet();        // Outputs: Hello, Guest!
greet('Alice'); // Outputs: Hello, Alice!

You can also accept and process all arguments at once, allowing you to work with any number of arguments passed. To do this, use the remainder operator (rest), which collects everything аргументы to array:

function sum(...numbers) {
    return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2, 3)); // Outputs: 6
console.log(sum(4, 5));    // Outputs: 9

If you really need to handle different argument configurations, you can always test them:

function findStudent(firstName, lastName) {
    if (typeof firstName === 'string' && typeof lastName === 'string') {
        // Find by first and last name
    } else if (typeof firstName === 'string') {
        // Find by first name
    } else {
        // Find all students
    }
}

findStudent('Alice', 'Johnson'); // Find by first and last name
findStudent('Bob');              // Find by first name
findStudent();                   // Find all

Also, don't forget that JavaScript has a built-in array arguments. Every function or method automatically provides a variable argumentscontaining all the arguments passed to the call.

Conclusion

As you get to know Node, you'll notice a variety of ways to solve almost every problem. The right approach is not always obvious, and sometimes there may be more than one valid solution for the same situation. Knowing about the different options available helps a lot in the job.

The ten JavaScript concepts discussed here are fundamentals that will be useful to every Node developer. However, this is just the tip of the iceberg. JavaScript is a powerful and complex language, and the more you use it, the more you will understand its vast capabilities and what you can create with it.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *