[Перевод] 10 концепций JavaScript, которыми должен овладеть каждый разработчик Node.js

e2b8d3196bde9f96446400bb81ea03b2.jpg

Данная статья является переводом

Node.js быстро стал стандартом для создания веб-приложений и системного ПО благодаря возможности использовать JavaScript на серверной стороне. Популярные фреймворки, такие как Express, и инструменты вроде Webpack способствуют его широкому распространению. Несмотря на существование конкурентов, таких как Deno и Bun, Node остается ведущей платформой для серверной разработки на JavaScript.

Мультипарадигмальная природа JavaScript позволяет использовать различные стили программирования, но также несет в себе риски, такие как проблемы с областью видимости и изменением объектов. Отсутствие оптимизации хвостовой рекурсии делает большие рекурсивные итерации опасными, а однопоточная архитектура Node требует использования асинхронного кода для повышения производительности. Однако, следуя ключевым концепциям и лучшим практикам, разработчики могут писать масштабируемый и эффективный код на Node.js.

1. Замыкания

Замыкание — это внутренняя функция в JavaScript, которая имеет доступ к области видимости внешней функции даже после завершения выполнения внешней функции. Замыкания делают переменные внутренней функции приватными. Функциональноепрограммирование стало очень популярным, поэтому замыкания являются важной частью инструментария разработчика на Node.js. Вот простой пример замыкания в JavaScript:

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

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

// the counter is now 3
  • Переменной count присваивается внешняя функция. Внешняя функция выполняется только один раз, устанавливая значение счетчика в ноль и возвращая внутреннюю функцию. Переменная _counter доступна только внутренней функции, что делает её поведение похожим на приватную переменную.

  • Пример здесь — это функция высшего порядка (или метафункция), то есть функция, которая принимает другую функцию или возвращает её. Замыкания встречаются во многих других приложениях. Замыкание происходит всякий раз, когда вы определяете функцию внутри другой функции, и внутренняя функция получает как свою собственную область видимости, так и доступ к области видимости родительской функции — то есть внутренняя функция «видит» переменные внешней функции, но не наоборот.

  • Это также удобно при использовании функциональных методов, таких как map(innerFunction), где внутренняя функция может использовать переменные, определённые во внешней области.

2. Прототипы

Каждая функция в JavaScript имеет свойство prototype, которое используется для добавления методов и свойств объектам. Это свойство не является перечисляемым, но позволяет разработчикам добавлять методы к объектам через их прототипы. JavaScript поддерживает наследование только через прототипы. Пример использования прототипов для добавления методов:

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

Хотя современный JavaScript имеет достаточно развитую поддержку классов, он всё равно использует систему прототипов «под капотом». Это является источником многой гибкости языка.

3. Приватные свойства с использованием хэш-имен

Раннее существовала практика добавления префикса в виде подчеркивания перед именем переменной, чтобы указать, что эта переменная должна быть приватной. Однако это было всего лишь соглашением, а не платформенно-задействованным ограничением. Современный JavaScript предлагает использование символа решетки # для создания приватных членов и методов в классах:

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

Приватные переменные, обозначенные через решетку, — это относительно новая, но очень полезная функция в JavaScript! Последние версии Node и современные браузеры поддерживают её, а инструменты разработчика в Chrome позволяют напрямую получать доступ к приватным переменным для удобства.

4. Приватные свойства с использованием замыканий

Вот еще один подход, который вы иногда можете встретить для обхода отсутствия приватных свойств в прототипной системе JavaScript — использование замыканий. Современный JavaScript позволяет определять приватные свойства, используя префикс в виде хэштега #, как показано в предыдущем примере. Однако это не работает в прототипной системе JavaScript. Тем не менее, такой прием вы часто можете увидеть в коде, и важно понимать, как он работает.

Определение приватных свойств с помощью замыканий позволяет вам имитировать приватные переменные. Методы, которым нужен доступ к этим приватным свойствам, должны быть определены непосредственно на объекте. Вот синтаксис для создания приватных свойств с использованием замыканий:

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

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

Этот метод позволяет защитить данные, делая их недоступными вне функции, обеспечивая таким образом их «приватность».

5. Модули

Когда в JavaScript не было системы модулей разработчики придумали хитрый трюк (называемый модульным паттерном), чтобы сделать что-то работающее. По мере эволюции JavaScript появились не одна, а две системы модулей: синтаксис подключения CommonJS и синтаксис require из ES6.

Node традиционно использовал CommonJS, в то время как браузеры используют ES6. Однако в последние годы новые версии Node также поддерживают ES6. Сейчас тенденция такова, что используются модули ES6, и когда-нибудь у нас будет только один синтаксис модулей для использования в JavaScript. ES6 выглядит так (где мы экспортируем модуль по умолчанию, а затем импортируем его):

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

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

Вы все еще можете встретить CommonJS, и иногда вам придется использовать его для импорта модуля. Вот как это выглядит, чтобы экспортировать, а затем импортировать модуль по умолчанию, используя CommonJS:

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

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

6. Обработка ошибок

Неважно, на каком языке или в какой среде вы работаете, обработка ошибок необходима и неизбежна. Node.js не является исключением. Существует три основных способа обработки ошибок: блоки try/catch, выбрасывание новых ошибок и обработчики on().

Блоки try/catch являются проверенным средством для захвата ошибок, когда что-то идет не так:

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

В этом случае мы выводим ошибку в консоль с помощью console.error. Вы можете выбрать выброс ошибки, передавая ее следующему обработчику. Обратите внимание, что это прерывает выполнение кода; то есть текущее выполнение останавливается, и следующий обработчик ошибок в стеке берет на себя управление:

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

Современный JavaScript предлагает довольно много полезных свойств на своих объектах Error, включая Error.stack для просмотра трассировки стека. В приведенном выше примере мы устанавливаем свойства Error.message и Error.cause с помощью аргументов конструктора.

Еще одно место, где вы найдете ошибки, это асинхронные блоки кода, где вы обрабатываете нормальные результаты с помощью .then(). В этом случае вы можете использовать обработчик on('error') или событие onerror, в зависимости от того, как promise возвращает ошибки. Иногда API возвращает объект ошибки в качестве второго значения вместе с нормальным значением. (Если вы используете await для асинхронного вызова, вы можете обернуть его в try/catch для обработки любых ошибок.) Вот простой пример обработки асинхронной ошибки:

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

Ни в коем случае не пропускайте ошибки! Я не буду показывать это здесь, потому что кто-то может скопировать и вставить это. В основном, если вы ловите ошибку и затем ничего не делаете, ваша программа будет молча продолжать работать без очевидного указания на то, что что-то пошло не так. Логика будет нарушена, и вы останетесь в недоумении, пока не найдете свой блок catch, в котором ничего нет. (Обратите внимание, что предоставление блока finally{} без блока catch приведет к пропуску ваших ошибок.)

7. Каррирование

Каррирование (currying) — это метод, делающий функции более гибкими. С помощью каррированной функции вы можете передать все ожидаемые аргументы и получить результат, или вы можете передать только часть аргументов и получить функцию, которая ждет оставшиеся аргументы. Вот простой пример каррирования:

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

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

Исходную каррированную функцию можно вызвать напрямую, передавая каждый из параметров в отдельной паре скобок, один за другим:

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"

Это интересная техника, которая позволяет создавать фабрики функций, где внешние функции позволяют частично настраивать внутренние. Например, вы также можете использовать вышеуказанную каррированную функцию следующим образом:

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

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

В реальном мире эта идея может быть полезной, когда вам нужно создать множество функций, которые различаются в зависимости от определенных параметров.

8. Методы call, apply и bind

Хотя мы не используем их каждый день, важно понимать, что такое методы call, apply и bind. Эти методы предоставляют серьезную гибкость языка. По сути, они позволяют вам указать, к чему разрешится ключевое слово this.

Во всех трех функциях первым аргументом всегда является значение this или контекст, который вы хотите передать функции.

Из всех трех методов call является самым простым. Это то же самое, что вызов функции с указанием ее контекста. Вот пример:

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"

Обратите внимание, что apply почти такой же, как call. Единственное различие состоит в том, что вы передаете аргументы в виде массива, а не по отдельности. Массивы легче манипулировать в JavaScript, что открывает больше возможностей для работы с функциями. Вот пример использования apply и 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"

Метод bind позволяет передавать аргументы функции без ее вызова. Возвращается новая функция с привязанными аргументами, предшествующими любым дальнейшим аргументам. Вот пример:

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. Мемоизация в JavaScript

Мемоизация — это техника оптимизации, которая ускоряет выполнение функции, сохраняя результаты дорогостоящих операций и возвращая кэшированные результаты при повторном появлении тех же наборов входных данных. Объекты JavaScript ведут себя как ассоциативные массивы, что упрощает реализацию мемоизации в JavaScript. Вот как преобразовать рекурсивную функцию вычисления факториала в мемоизированную функцию вычисления факториала:

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)

Немедленно вызываемое функциональное выражение (IIFE) — это функция, которая выполняется сразу после создания. Оно не связано с событиями или асинхронным выполнением. Вы можете определить IIFE следующим образом:

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

Первая пара скобок function(){...} преобразует код внутри скобок в выражение. Вторая пара скобок вызывает функцию, полученную из выражения. IIFE также можно описать как самовызывающуюся анонимную функцию. Чаще всего оно используется для ограничения области видимости переменной, созданной с помощью var, или для инкапсуляции контекста, чтобы избежать конфликтов имен.

Существуют также ситуации, когда нужно вызвать функцию с использованием await, но вы не находитесь внутри асинхронного блока функции. Это бывает в файлах, которые вы хотите сделать исполняемыми напрямую, а также импортируемыми как модуль. Вы можете обернуть такой вызов функции в блок IIFE следующим образом:

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

11. Полезные функции для работы с аргументами

Хотя JavaScript не поддерживает перегрузку методов (так как функции могут принимать произвольное количество аргументов), он обладает несколькими мощными возможностями для работы с аргументами.

Во-первых, вы можете определить функцию или метод с значениями по умолчанию:

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

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

Также можно принимать и обрабатывать все аргументы одновременно, что позволяет работать с любым количеством переданных аргументов. Для этого используется оператор остатка (rest), который собирает все аргументы в массив:

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

Если действительно нужно обрабатывать разные конфигурации аргументов, всегда можно проверить их:

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

Кроме того, не забывайте, что в JavaScript есть встроенный массив arguments. Каждая функция или метод автоматически предоставляет переменную arguments, содержащую все переданные аргументы при вызове.

Заключение

По мере того как вы знакомитесь с Node, вы заметите множество способов решения практически каждой проблемы. Правильный подход не всегда очевиден, и иногда для одной и той же ситуации может существовать несколько действительных решений. Знание о различных доступных вариантах значительно помогает в работе.

Десять концепций JavaScript, обсуждаемых здесь, являются основами, которые будут полезны каждому разработчику Node. Однако это только верхушка айсберга. JavaScript — мощный и сложный язык, и чем больше вы его используете, тем глубже будете понимать его обширные возможности и то, что можно с его помощью создать.

© Habrahabr.ru