Что следует избегать в JavaScript

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

Многие неопытные (и даже опытные) фронтенд и бэкенд-разработчики допускают ошибки, которые могут привести к багам или проблемам с надёжностью. Ниже перечислены вещи, которых, следует избегать, чтобы снизить риск ошибок. Всё, что здесь перечислено, не является абсолютным правилом, есть ситуации, когда это может быть допустимо или даже необходимо (производительность, гибкость и т. д.). Это скорее совет.

Использование element.innerHTML для установки отображаемого текста

Разработчики часто применяют element.innerHTML для изменения текста в элементах DOM. Однако этот метод может работать некорректно, если текст не является экранированным HTML-кодом:

document.body.innerHTML = '<button>456</button>' // кнопка с текстом "456"

Для исправления этой ситуации используйте element.innerText или, что ещё лучше, element.textContent, поскольку последний не обрабатывает стили (например, пропускает текст в скрытых элементах) и имеет лучшую производительность (при получении значения):

document.body.textContent = '<button>456</button>' // текст "<button>456</button>"

Результат будет: <button>456</button>, как и ожидалось.

Если необходимо экранировать HTML в средах без DOM API, таких как NodeJS, можно сделать следующее:

function escapeHtml(htmlStr) {
return htmlStr?.toString()
.replaceAll('&', "&amp;")
.replaceAll('<', "&lt;")
.replaceAll('>', "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;") ?? '';
}

console.log(escapeHtml('<button>456</button>')); // &lt;button&gt;456&lt;/button&gt;

Использование JSON.parse(JSON.stringify(object)) для клонирования объектов

Многие из нас использовали JSON.stringify(), а затем передавали полученную строку в JSON.parse(), для выполнения глубокого копирования объекта. Этот подход может быть подходящим для простых случаев, однако многие не осознают потенциальные ограничения JSON.stringify() и JSON.parse(). Вот где могут возникнуть проблемы при использовании JSON.stringify() / JSON.parse():

  1. JSON.stringify() не поддерживает некоторые значения, такие как NaN или undefined. Они могут быть пропущены или преобразованы в null. Для некоторых типов данных, таких как bigint, это приведёт к выбросу исключения.
  2. JSON.stringify() не может работать с объектами, содержащими циклические ссылки:
    const obj = {};
    obj.selfReference = obj;
    console.log(JSON.stringify(obj)); // исключение
  3. Хотя обычно это не так серьёзно, как первые два случая, но следует отметить, что для больших объектов этот метод неэффективен. Он медленный и тратит много памяти.

По возможности следует отдавать предпочтение structuredClone(). structuredClone() также автоматически обрабатывает самовложения / циклические структуры.

const obj = {};
obj.selfReference = obj;
const clonedObj = structuredClone(obj);
console.log(obj === clonedObj);
// false, потому что это клонированный объект с другим адресом в памяти

console.log(clonedObj.selfReference === clonedObj);
// true, потому что он имеет ту же структуру, что и obj (изоморфен по отношению к obj, т. е. как граф)

Сравнение объектов с помощью JSON.stringify()

Это связано с предыдущим пунктом. Помимо описанных выше проблем JSON.stringify(), сравнение может работать некорректно из-за разного порядка свойств:

const obj1 = {a: '1', b: '2'};
const obj2 = {b: '2', a: '1'};

console.log(JSON.stringify(obj1)); // {"a":"1","b":"2"}
console.log(JSON.stringify(obj2)); // {"b":"2","a":"1"}

Чрезмерное использование обычных объектов в качестве ассоциативных массивов

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

  1. Динамическое изменение объекта негативно влияет на производительность, поскольку объекты оптимизированы для фиксированной структуры (или формы).
  2. По умолчанию объекты JavaScript имеют одно свойство, которое ведёт себя совершенно иначе. Это свойство называется Object.prototype.__proto__. Object.prototype.__proto__ — это геттер и сеттер, получающий или устанавливающий прототип объекта. И этот факт делает использование объектов в качестве сопоставлений ключ-значение опасным, поскольку свойство __proto__ будет вести себя совсем по-другому. __proto__ является источником неприятных проблем безопасности, называемых "загрязнением прототипа".

Примеры ситуаций, в которых установка или получение __proto__ может вызвать проблемы:

const obj = {};

obj["key1"] = "aaa";
console.log(obj["key1"]); // "aaa", отлично
obj["key2"] = "bbb";
console.log(obj["key2"]); // "bbb", отлично
obj["__proto__"] = "ccc";
console.log(obj["__proto__"]); // { ... }, если __proto__ установлено в значение, не являющееся объектом, ничего не произойдёт
obj["__proto__"] = { a: 5 };
console.log(obj["a"]); // 5, объект наследует свойства __proto__
obj["__proto__"] = obj; // Исключение из-за циклической прото цепочки

Один из способов исправить это — избавиться от __proto__, установив его значение равным null (в качестве бонуса это также избавит от других свойств и методов, оставив гораздо более чистый объект):

const obj = { __proto__: null };

obj["key1"] = "aaa";
console.log(obj["key1"]); // "aaa", отлично
obj["key2"] = "bbb";
console.log(obj["key2"]); // "bbb", отлично
obj["__proto__"] = "ccc";
console.log(obj["__proto__"]); // "ccc", теперь всё отлично
obj["__proto__"] = { a: 5 };
console.log(obj["a"]); // undefined, теперь объект не наследует свойства
obj["__proto__"] = obj; // Без исключений, циклические ссылки работают нормально.

Однако, учитывая эти проблемы, лучше использовать Map вместо обычных объектов.

Использование eval() для выполнения кода

eval() — это ужасный способ выполнения кода, и не следует его использовать. Причина в том, что eval() также может получить доступ к локальным переменным, что означает меньшую безопасность и более медленное выполнение:

function f() {
let a = 1;
eval("++a");
console.log(a); // 2
}

f();

Вместо этого используйте Function:

{
// Простая функция без аргументов
const f1 = new Function('console.log("f1")');

// A функция с аргументами и оператором return
const f2 = new Function('a', 'b', 'return a * b;');

// Функция с аргументами по умолчанию и оператором return
const f3 = new Function('a = 1', '{b = 2, c = 3} = {}', 'd = 4', 'return [a, b, c, d];');

let a = 1;

// Функция, пытающаяся получить доступ к локальной переменной
const f4 = new Function('++a');


f1(); // "f1"
console.log(f2(6, 8)); // 48
console.log(f3()); // [1, 2, 3, 4]
console.log(f3(10, { c: 30 })); // [10, 2, 30, 4]
f4(); // Ошибка, поскольку "a" не является глобальной переменной.
}

Добавление чего-либо к innerHTML, innerText или textContent элемента

Когда вы добавляете что-либо к innerHTML, innerText или textContent какого-либо элемента, выполняя операцию типа element.innerHTML += "какой-либо html код", все существующие дочерние элементы будут удалены, а новые дочерние элементы будут созданы с нуля. Это значительно замедлит работу и может привести к сбросу состояний существующих дочерних элементов, слушателей событий и т. д.

Правильный способ добавления/вставки в начало — использование element.insertAdjacentHTML() или element.insertAdjacentText():

const parent1 = document.createElement('div');
const parent2 = document.createElement('div');
const child1 = document.createElement('div');
const child2 = document.createElement('div');

parent1.append(child1);
parent2.append(child2);

console.log(parent1.innerHTML); // <div></div>
console.log(parent2.innerHTML); // <div></div>

child1.insertAdjacentHTML('beforebegin', '<b>1</b>'); // добавление html перед элементом
child1.insertAdjacentHTML('afterbegin', '<b>2</b>'); // добавление html в начало элемента
child1.insertAdjacentHTML('beforeend', '<b>3</b>'); // добавление html в конец элемента
child1.insertAdjacentHTML('afterend', '<b>4</b>'); // добавление html после элемента


child2.insertAdjacentText('beforebegin', '<1>'); // добавление текста перед элементом
child2.insertAdjacentText('afterbegin', '<2>'); // добавление текста в начало элемента
child2.insertAdjacentText('beforeend', '<3>'); // добавление текста в конец элемента
child2.insertAdjacentText('afterend', '<4>'); // добавление текста после элемента

console.log(parent1.innerHTML); // <b>1</b><div><b>2</b><b>3</b></div><b>4</b>
console.log(parent2.innerHTML); // &lt;1&gt;<div>&lt;2&gt;&lt;3&gt;</div>&lt;4&gt;

Не использование модулей

Сейчас 2025 год, и основные браузеры поддерживают модули JavaScript уже более 7 лет. Использование тегов <script> без атрибута type="module" в значительной степени устарело. Атрибут type="module" указывает браузерам загружать скрипт как модуль. Модули имеют несколько преимуществ:

Отдавайте предпочтение модулям для основного исходного кода вашего сайта.

Использование var

Было время, когда использование let и const приводило к значительному снижению производительности. Причина заключалась в том, что JavaScript должен был убедиться, что переменные, объявленные с помощью let или const, доступны только после их объявления, что вынуждало компиляторы JavaScript выполнять дополнительные проверки (проверки временной мёртвой зоны, сокращённо TDZ) при доступе к таким переменным. Наиболее известным примером было 10-процентное улучшение производительности при замене let или const на var. Даже сегодня let или const могут вызывать некоторые накладные расходы, но в последние годы движки JavaScript провели огромную оптимизацию, которая устранила проверки TDZ во многих ситуациях. Таким образом, использование var приносит больше вреда, чем пользы в подавляющем большинстве ситуаций из-за его плохой области видимости. Используйте var только в том случае, если это код, чувствительный к производительности, и только когда тесты действительно показывают улучшение производительности с var.

Использование нестрогого равенства или неравенства (== или !=)

Невозможно сосчитать количество мемов о бессмысленности нестрогого равенства (==) в JavaScript. Один из самых забавных мемов — это «Святая Троица» нестрогих сравнений:

«Святая троица» JavaScript
«Святая троица» JavaScript

Использование == или != вместо === или !== иногда приводит к непредвиденному поведению. Поэтому отдавайте предпочтение === или !==, если не уверены на 100% в том, что делаете.

Конкатенация строк с помощью оператора + при вставке некоторых значений

При объединении строк для вставки некоторых значений предпочтительно использовать интерполяцию с шаблонными литералами (`... ${некоторое значение} ...`), когда это возможно. Это сделает код более читабельным. Так, вместо этого:

const name = prompt("What is your name?");
alert("Hi, " + name + ", nice to see you!");

Лучше сделать так:

const name = prompt("What is your name?");
alert(`Hi, ${name}, nice to see you!`);

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

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

function connectToDb(
dbUrl, username, password, encoding = 'utf-8',
portNumber = 3000, timeout = 30
) {
// ...
}

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

connectToDb('jdbc:mysql://some_host', 'some_username', 'some_pass', 'utf-8', 3000, 20);

Эту проблему можно решить с помощью синтаксиса деструктуризации:

function connectToDb(
dbUrl, username, password,
{ encoding = 'utf-8', portNumber = 3000, timeout = 30 } = {}
) {
console.log(dbUrl, username, password, encoding,
portNumber, timeout);
// ...
}

connectToDb('jdbc:mysql://some_host', 'some_username', 'some_pass', { timeout: 20 });
// 'jdbc:mysql://some_host', 'some_username', 'some_pass', 'utf-8', 3000, 20

connectToDb('jdbc:mysql://some_host', 'some_username', 'some_pass');
// 'jdbc:mysql://some_host', 'some_username', 'some_pass', 'utf-8', 3000, 30

Использование объектов "обёрток" для примитивных значений

JavaScript поддерживает объекты "обёртки" для примитивных значений, таких как Number, Boolean, String и BigInt. Каждый раз, когда вы вызываете метод или получаете доступ к свойству из примитивного значения, значение неявно "оборачивается" в специальный объект. Это происходит потому, что, в отличие от объектов, примитивные значения не могут иметь собственных свойств. Этот механизм неявного преобразования иногда называют «autoboxing»/«автобоксинг» . Механизм «автобоксинга» даже позволяет расширять или исправлять поведение методов и свойств примитивных значений путём исправления прототипов Number, String, Boolean или BigInt:

Boolean.prototype.saySomething = function() {console.log(`I'm a boolean`, this.valueOf())};
Number.prototype.saySomething = function() {console.log(`I'm a number`, this.valueOf())};
String.prototype.saySomething = function() {console.log(`I'm a string`, this.valueOf())};
BigInt.prototype.saySomething = function() {console.log(`I'm a bigint`, this.valueOf())};

true.saySomething();
(1).saySomething(); // находится в скобках, потому что "." интерпретируется как плавающая точка
"1".saySomething();
1n.saySomething();

Несмотря на то, что это звучит неплохо, следует избегать использования их в качестве примитивных заменителей значений, поскольку они ведут себя по-разному и могут негативно влиять на производительность:

const a = 1, b = 1;
const wrapperOfA = new Number(a), wrapperOfB = new Number(b);
console.log(a === b); // true, числа сравниваются по их значениям
console.log(wrapperOfA === wrapperOfB ); // false, поскольку объекты сравниваются по их ссылкам
console.log(wrapperOfA == wrapperOfB ); // false, даже нестрогое сравнение не помогает, поскольку оно делает то же самое

Что интересно, даже комитет EcmaScript считал объекты "обёртки" опасными и решил сделать невозможным явное создание объектов BigInt при введении bigint:

new BigInt(1n); // исключение

Заключение

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

Комментарии


Дополнительные материалы

Предыдущая Статья

ECMAScript 2025: Что нового