Почему опасно менять прототипы базовых типов в JS
Почему опасно писать прямо в прототипы базовых типов?
Теория: прототипы и цепочка
В JavaScript у объектов есть внутренняя ссылка на прототип (часто объясняется как “цепочка прототипов”), по которой движок ищет свойства и методы, если их нет в самом объекте. У массивов методы лежат в Array.prototype, у строк — в String.prototype, и почти все объекты в итоге наследуют от Object.prototype.
Механизм поиска свойства упрощённо выглядит так: сначала проверяются собственные свойства объекта, затем свойства прототипа, затем прототипа прототипа и так далее, пока цепочка не закончится.
Схема поиска свойства:
Object instance
|
v
(собственные свойства)
|
v
SomeType.prototype
|
v
Object.prototype
|
v
null
SomeType.prototype меняет “общую базу методов” для всех экземпляров этого типа, включая уже созданные.Почему это опасно
Глобальные побочные эффекты
Добавление, например, Array.prototype.myMethod влияет на все массивы в приложении и во всех подключённых зависимостях. Из-за этого ошибка может проявиться в другом месте: код меняется в одном файле, а ломается логика в библиотеке, которая никогда не ожидала появления нового свойства с таким именем.
Пример “глобальности”:
/ Изменение прототипа массива (опасная практика)
Array.prototype.sum = function () {
return this.reduce((a, b) => a + b, 0);
};
console.log([1, 2, 3].sum()); // 6
// Важно: метод появился у всех массивов, включая те, что создаются в стороннем коде
Конфликты имён (с библиотеками и будущим стандартом)
Если добавляется метод с популярным именем, то существует риск, что:
- библиотека уже добавляет метод с таким же именем, но с другой логикой;
- другая часть проекта рассчитывает, что такого метода не существует;
- среда выполнения позже добавит стандартный метод с таким именем, но с иным контрактом.
Пример конфликта имён (идея упрощена):
// Приложение добавляет метод с коротким именем
String.prototype.format = function () {
return "локальная логика форматирования";
};
// Где-то в зависимости ожидается другое поведение format()
// или в будущем стандарт добавит одноимённый метод с другой семантикой
console.log("x".format());
Неожиданное перечисление свойств
Некоторые конструкции перебирают свойства, включая унаследованные (например, for...in). Если в прототип добавлено перечислимое свойство, оно может внезапно появиться в переборе и сломать алгоритм, который ожидает только индексы массива или только реальные поля объекта.
Пример ошибки при перечислении:
// Добавление перечислимого свойства в прототип (опасно)
Array.prototype.extra = 123;
const arr = [10, 20];
for (const k in arr) {
// Ожидаются "0" и "1", но также может появиться "extra"
console.log(k);
}
Даже если метод добавляется через Object.defineProperty(..., { enumerable: false }), остаются другие риски: глобальное изменение поведения, конфликты имён, влияние на проверки наличия свойства и на совместимость.
Риски безопасности: prototype pollution
Prototype pollution — ситуация, когда из-за обработки непроверенных данных в прототипы (часто в Object.prototype) попадают новые свойства. После этого эти свойства становятся “видимыми” для многих объектов и способны повлиять на проверки, условия и настройки по всему приложению.
Типичный источник проблемы — “слияние” объектов, пришедших извне (например, из JSON), без фильтрации опасных ключей.
Пример, демонстрирующий идею (упрощённо):
function merge(target, source) {
for (const key in source) {
target[key] = source[key];
}
return target;
}
// Непроверенный ввод
const payload = JSON.parse('{"__proto__":{"isAdmin":true}}');
const user = {};
merge(user, payload);
// Опасный эффект: свойство может начать "как бы существовать" у многих объектов
const obj = {};
console.log(obj.isAdmin); // потенциально true в уязвимых сценариях
if (obj.isAdmin) ... без строгой валидации источника этого значения.Как правильно решать задачу
Надёжнее не менять встроенные прототипы, а выносить функциональность в обычные функции, модули или классы-обёртки. Такой код проще тестировать, проще читать и невозможно “случайно” навязать всем объектам в программе.
Утилитные функции
Пример замены Array.prototype.unique на обычную функцию:
function unique(arr) {
return Array.from(new Set(arr));
}
console.log(unique([1, 1, 2])); // [1, 2]
Плюсы: отсутствуют глобальные побочные эффекты, проще типизировать, проще переиспользовать, легче сопровождать.
Безопасные структуры вместо “словаря на Object”
Если необходима структура “ключ → значение”, часто безопаснее Map, потому что он не использует Object.prototype как часть механики доступа к ключам.
const m = new Map();
m.set("__proto__", "ok");
console.log(m.get("__proto__")); // "ok"
Полифилы только по строгой необходимости
Единственный относительно оправданный случай изменения прототипа — полифил стандартного метода для старого окружения, когда:
- метод отсутствует;
- добавление точно повторяет контракт стандарта;
- свойство делается неперечислимым;
- решение документировано и контролируется (обычно в одном месте проекта).
Пример “аккуратного” добавления (упрощённая форма):
if (!Array.prototype.myPolyfilledMethod) {
Object.defineProperty(Array.prototype, "myPolyfilledMethod", {
value: function () {
return "реализация";
},
enumerable: false,
writable: true,
configurable: true
});
}
Защита при слиянии объектов
При обработке внешнего ввода следует исключать опасные ключи (__proto__, constructor, prototype) и по возможности создавать объекты без прототипа для роли “чистого словаря”.
Пример:
function safeAssign(target, source) {
for (const key of Object.keys(source)) {
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
target[key] = source[key];
}
return target;
}
const dict = Object.create(null);
safeAssign(dict, JSON.parse('{"a":1,"__proto__":{"polluted":true}}'));
console.log(dict.a); // 1
Таблица рисков и замен
| Риск | Как проявляется | Безопасная альтернатива |
|---|---|---|
| Глобальные изменения поведения | “Ломается” код в другом модуле или зависимости | Утилитные функции, отдельные модули |
| Конфликты имён | Одинаковое имя метода, разный контракт | Явный импорт функции, неймспейсы |
| Неожиданное перечисление | Лишние ключи в for...in, некорректные проверки | Object.keys(), строгая проверка собственных свойств |
| Prototype pollution | Внешний ввод влияет на прототипы и проверки | Фильтрация ключей, Object.create(null), Map |
В итоге: опасность записи прямо в прототипы базовых типов (например, Object.prototype, Array.prototype, String.prototype) заключается в том, что меняется поведение сразу у всех значений данного типа во всей программе, включая сторонние библиотеки и будущие обновления среды. Это приводит к труднообнаружимым ошибкам, конфликтам имён, проблемам совместимости и иногда к уязвимостям, связанным с загрязнением прототипов (prototype pollution).