Пошук уроків, статей та іншого контенту
Проаналізуєте особливості вбудованих прототипів, Symbol.hasInstance і ризики зміни Object.prototype у прикладних системах.
У JavaScript об’єкти зазвичай отримують властивості та методи через ланцюжок прототипів. Якщо властивість не знайдена безпосередньо в об’єкті, рушій шукає її в його прототипі, потім у прототипі прототипу і так далі, доки не дійде до null.
const user = {
name: "Olena"
};
console.log(user.hasOwnProperty("name")); // true
console.log(user.toString()); // [object Object]У цьому прикладі:
name — власна властивість user;
hasOwnProperty зазвичай успадкована від Object.prototype;
toString також знайдена в Object.prototype;
ланцюжок завершується значенням null.
Приблизний ланцюжок для звичайного об’єкта:
user
↓
Object.prototype
↓
nullДля масиву:
array
↓
Array.prototype
↓
Object.prototype
↓
nullДля функції:
function
↓
Function.prototype
↓
Object.prototype
↓
nullПрототип — це не клас у традиційному сенсі. Класи в JavaScript є синтаксичним і семантичним шаром над прототипним механізмом.
До вбудованих прототипів належать, зокрема:
Object.prototype;
Array.prototype;
Function.prototype;
String.prototype;
Number.prototype;
Boolean.prototype;
RegExp.prototype;
Date.prototype;
Map.prototype;
Set.prototype;
Promise.prototype;
прототипи типізованих масивів;
прототипи помилок, наприклад Error.prototype.
Властивості цих об’єктів доступні екземплярам через успадкування:
const values = [10, 20, 30];
console.log(values.map(value => value * 2)); // [20, 40, 60]
console.log(values.hasOwnProperty("map")); // false
console.log("map" in values); // trueМетод map не є власною властивістю values. Він розташований у Array.prototype.
Для розрізнення власних властивостей використовуйте:
const object = {
ownProperty: 42
};
console.log(Object.hasOwn(object, "ownProperty")); // true
console.log(Object.hasOwn(object, "toString")); // false
console.log("toString" in object); // truein перевіряє весь ланцюжок прототипів, а Object.hasOwn — лише сам об’єкт.
Старий варіант:
object.hasOwnProperty("ownProperty");може бути ненадійним, якщо:
об’єкт не має прототипу;
властивість hasOwnProperty була перевизначена;
дані походять із ненадійного джерела.
Надійніший сучасний варіант:
Object.hasOwn(object, "propertyName");Або сумісний із давнішими середовищами варіант:
Object.prototype.hasOwnProperty.call(object, "propertyName");Для дослідження прототипного ланцюжка використовують Object.getPrototypeOf:
const array = [];
console.log(Object.getPrototypeOf(array) === Array.prototype); // true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // nullinstanceof перевіряє не тип значення в абстрактному сенсі, а наявність prototype правої частини в ланцюжку прототипів лівої частини:
const values = [];
console.log(values instanceof Array); // true
console.log(values instanceof Object); // trueЕквівалентна логіка приблизно така:
function isInPrototypeChain(value, prototype) {
if (value === null || (typeof value !== "object" && typeof value !== "function")) {
return false;
}
let current = Object.getPrototypeOf(value);
while (current !== null) {
if (current === prototype) {
return true;
}
current = Object.getPrototypeOf(current);
}
return false;
}
const values = [];
console.log(isInPrototypeChain(values, Array.prototype)); // true
console.log(isInPrototypeChain(values, Object.prototype)); // trueЦе лише спрощена модель. Реальний оператор instanceof також враховує Symbol.hasInstance.
Методи вбудованих типів часто мають спеціальні внутрішні вимоги. Наприклад, метод Map.prototype.get очікує, що його this буде об’єктом, ініціалізованим як Map.
const map = new Map([["language", "JavaScript"]]);
console.log(map.get("language")); // JavaScript
const get = map.get;
try {
console.log(get("language"));
} catch (error) {
console.log(error.name); // TypeError
}Втрата контексту this є типовою причиною помилок під час передачі методів як callback-функцій.
Також не кожну властивість вбудованого прототипу можна безпечно замінювати. Деякі властивості:
мають спеціальні внутрішні алгоритми;
є небажаними для перевизначення;
можуть бути невидимими при звичайному переборі;
мають певні дескриптори властивостей.
Перевірити дескриптор можна так:
const descriptor = Object.getOwnPropertyDescriptor(
Array.prototype,
"map"
);
console.log(descriptor);
// {
// value: [Function: map],
// writable: true,
// enumerable: false,
// configurable: true
// }Не всі властивості мають однакові дескриптори, тому перед низькорівневими змінами потрібно перевіряти їх явно.
Symbol.hasInstanceSymbol.hasInstance — well-known symbol, який визначає поведінку оператора instanceof.
Коли виконується:
value instanceof ConstructorJavaScript може викликати:
Constructor[Symbol.hasInstance](value)За замовчуванням функції успадковують реалізацію Symbol.hasInstance від Function.prototype.
instanceofКлас може визначити статичний метод [Symbol.hasInstance]:
class PositiveNumber {
static [Symbol.hasInstance](value) {
return typeof value === "number" && value > 0;
}
}
console.log(10 instanceof PositiveNumber); // true
console.log(-10 instanceof PositiveNumber); // false
console.log("10" instanceof PositiveNumber); // falseЦей приклад важливий: PositiveNumber не створює екземпляри, які фізично мають PositiveNumber.prototype у ланцюжку. Оператор instanceof використовує спеціально визначене правило.
Symbol.hasInstance може бути корисним для структурної перевірки:
class Serializable {
static [Symbol.hasInstance](value) {
return (
value !== null &&
typeof value === "object" &&
typeof value.serialize === "function"
);
}
}
const document = {
serialize() {
return JSON.stringify(this);
}
};
console.log(document instanceof Serializable); // trueОднак така перевірка не доводить, що об’єкт створений конкретним класом. Вона лише визначає, що об’єкт відповідає певному контракту.
Якщо клас не визначає власний Symbol.hasInstance, він використовує успадковану поведінку:
class Base {}
class Child extends Base {}
const value = new Child();
console.log(value instanceof Child); // true
console.log(value instanceof Base); // trueДля стандартної реалізації важливе значення має Child.prototype. Якщо цей прототип змінити, результат instanceof також може змінитися.
class User {}
const user = new User();
console.log(user instanceof User); // true
const oldPrototype = User.prototype;
User.prototype = {};
console.log(user instanceof User); // false
console.log(oldPrototype.isPrototypeOf(user)); // trueУ прикладному коді не слід змінювати prototype конструктора після створення екземплярів без чіткої причини. Це може зламати перевірки типів і поведінку методів.
instanceofinstanceof не завжди є універсальною перевіркою типу.
Об’єкти з різних JavaScript realm, наприклад із різних iframe, можуть мати різні екземпляри вбудованих конструкторів:
value instanceof Arrayможе повернути false, навіть якщо value є масивом, створеним в іншому realm.
Для перевірки вбудованих типів часто краще використовувати:
Array.isArray(value);Для інших типів можна застосовувати Object.prototype.toString, але слід пам’ятати, що його поведінка також може бути змінена через Symbol.toStringTag:
console.log(Array.isArray([])); // true
console.log(typeof new Map()); // object
console.log(Object.prototype.toString.call(new Map())); // [object Map]instanceof перевіряє ланцюжок прототипів, а не походження даних:
const object = {};
Object.setPrototypeOf(object, Array.prototype);
console.log(object instanceof Array); // true
console.log(Array.isArray(object)); // falseОб’єкт лише отримав Array.prototype у своєму ланцюжку, але не став справжнім масивом. Це демонструє, чому instanceof і перевірка внутрішньої структури об’єкта — не одне й те саме.
Object.createObject.create дає змогу створити об’єкт із заданим прототипом:
const baseUser = {
describe() {
return `Користувач: ${this.name}`;
}
};
const admin = Object.create(baseUser);
admin.name = "Olena";
console.log(admin.describe()); // Користувач: Olena
console.log(Object.getPrototypeOf(admin) === baseUser); // trueОсобливий випадок — об’єкт без прототипу:
const dictionary = Object.create(null);
dictionary.language = "JavaScript";
console.log(dictionary.language); // JavaScript
console.log(Object.getPrototypeOf(dictionary)); // nullТакий об’єкт не має успадкованих властивостей:
console.log("toString" in dictionary); // false
console.log("constructor" in dictionary); // falseОб’єкти без прототипу корисні як словники, коли ключі походять із зовнішніх даних. Вони зменшують ризик конфліктів із властивостями Object.prototype.
Водночас вони не мають звичайних методів об’єкта:
const dictionary = Object.create(null);
console.log(typeof dictionary.toString); // undefinedТому для роботи з ними використовуйте статичні методи Object:
console.log(Object.keys(dictionary));
console.log(Object.hasOwn(dictionary, "language"));Object.prototypeТехнічно можна додати власну властивість до Object.prototype:
Object.prototype.isNonEmpty = function () {
return Object.keys(this).length > 0;
};Після цього метод стане доступним майже для кожного звичайного об’єкта:
const user = {
name: "Olena"
};
console.log(user.isNonEmpty()); // trueАле така зміна має глобальний ефект. Вона впливає на:
код усієї програми;
сторонні бібліотеки;
об’єкти, створені іншими модулями;
цикли for...in;
перевірки наявності властивостей;
серіалізацію та логіку зіставлення ключів.
Якщо бібліотека або майбутня версія середовища додасть властивість із таким самим іменем, виникне конфлікт:
Object.prototype.serialize = function () {
return "global behavior";
};
const object = {
serialize: "дані"
};
console.log(object.serialize); // даніВласна властивість об’єкта приховує успадковану, але різні частини системи можуть очікувати різну поведінку.
for...inДодані властивості можуть з’явитися під час перебору:
Object.prototype.sharedFlag = true;
const settings = {
theme: "dark"
};
for (const key in settings) {
console.log(key);
}
// theme
// sharedFlagЩоб перебирати лише власні enumerable-властивості, перевіряйте їх:
for (const key in settings) {
if (Object.hasOwn(settings, key)) {
console.log(key);
}
}Ще краще для звичайного переліку ключів використовувати:
for (const key of Object.keys(settings)) {
console.log(key);
}Після демонстрації глобальну зміну потрібно було б видалити:
delete Object.prototype.sharedFlag;Але в реальному застосунку сам факт такої зміни вже може спричинити непередбачувані наслідки.
Бібліотеки часто припускають, що об’єкти мають стандартну поведінку. Якщо змінити методи toString, valueOf, hasOwnProperty або інші властивості, можна отримати:
неправильне логування;
помилки під час порівняння;
непередбачуване форматування;
зламану валідацію;
зміну поведінки внутрішнього коду бібліотек.
Глобальне розширення прототипів називають monkey patching. У спеціалізованому середовищі воно іноді виправдане, але в прикладних системах зазвичай створює більше ризиків, ніж користі.
Prototype pollution — це клас уразливостей, за якого ненадійні дані змінюють прототипи об’єктів, часто через Object.prototype.
Небезпечним може бути код, який рекурсивно записує довільні ключі з вхідних даних:
function assignPath(target, path, value) {
const parts = path.split(".");
let current = target;
for (let index = 0; index < parts.length - 1; index += 1) {
const part = parts[index];
if (!current[part]) {
current[part] = {};
}
current = current[part];
}
current[parts.at(-1)] = value;
}
const configuration = {};
assignPath(configuration, "__proto__.isAdmin", true);
console.log({}.isAdmin); // true у середовищах, де цей шлях змінює Object.prototypeЦе небезпечно, оскільки властивість може з’явитися в об’єктах, які не мають жодного стосунку до конфігурації.
Потенційні наслідки:
обходи перевірок на кшталт if (user.isAdmin);
зміна логіки авторизації;
несподівані властивості в об’єктах;
помилки сторонніх пакетів;
у деяких сценаріях — виконання небезпечної поведінки.
Конкретний результат залежить від реалізації функції, версії середовища та способу запису властивостей, але сам патерн слід вважати небезпечним.
Не дозволяйте спеціальні ключі, якщо вони не потрібні:
const forbiddenKeys = new Set([
"__proto__",
"prototype",
"constructor"
]);
function isSafeKey(key) {
return typeof key === "string" && !forbiddenKeys.has(key);
}
function copyOwnProperties(target, source) {
for (const key of Object.keys(source)) {
if (!isSafeKey(key)) {
continue;
}
target[key] = source[key];
}
return target;
}
const result = copyOwnProperties({}, {
theme: "dark",
"__proto__": {
polluted: true
}
});
console.log(result.theme); // darkУ цьому прикладі синтаксис об’єктного літерала має особливості, тому на практиці потрібно також контролювати джерело даних і спосіб його парсингу. Важливо захищати саме всі шляхи оновлення об’єктів, а не лише один helper.
const counts = Object.create(null);
counts.apple = 2;
counts.orange = 3;
console.log(counts.apple); // 2Краще явно описати дозволені поля:
function parseOptions(input) {
return {
timeout: typeof input.timeout === "number"
? input.timeout
: 5000,
retries: typeof input.retries === "number"
? input.retries
: 3
};
}Явне копіювання дозволених значень безпечніше за копіювання всіх властивостей із зовнішнього об’єкта.
in для перевірки данихНебезпечний варіант:
if ("isAdmin" in user) {
// Властивість може бути успадкована, а не передана користувачем.
}Надійніший варіант:
if (Object.hasOwn(user, "isAdmin") && user.isAdmin === true) {
// Перевірка власної властивості.
}Для критичних дозволів сам факт наявності прапорця в об’єкті не повинен бути єдиним механізмом авторизації.
Замість:
Object.prototype.toSafeString = function () {
return String(this);
};використовуйте окрему функцію:
function toSafeString(value) {
return String(value);
}
console.log(toSafeString(42)); // 42Переваги:
немає глобальної зміни;
зрозумілий контракт;
легше тестувати;
немає конфліктів імен.
const arrayUtils = {
last(array) {
return array[array.length - 1];
},
compact(array) {
return array.filter(Boolean);
}
};
console.log(arrayUtils.last([1, 2, 3])); // 3
console.log(arrayUtils.compact([0, 1, false, 2])); // [1, 2]Якщо поведінка справді належить певному типу, створіть окремий клас:
class UserList extends Array {
active() {
return this.filter(user => user.active === true);
}
}
const users = new UserList(
{ name: "Olena", active: true },
{ name: "Andrii", active: false }
);
console.log(users.active()); // [{ name: "Olena", active: true }]Цей підхід ізолює розширення від усіх масивів у програмі.
Symbol для спеціалізованого протоколуЯкщо потрібно інтегрувати власний тип із мовним оператором або стандартною операцією, використовуйте відповідний well-known symbol, а не довільну глобальну властивість.
Наприклад, Symbol.hasInstance може бути доречним для контрольованої перевірки сумісності з абстрактним типом. Але його не слід використовувати лише для створення складнішої форми звичайної перевірки типів.
У великих системах іноді заморожують об’єкти або прототипи:
"use strict";
const configuration = Object.freeze({
environment: "production"
});
try {
configuration.environment = "development";
} catch (error) {
console.log(error.name); // TypeError
}
console.log(configuration.environment); // productionObject.freeze:
забороняє змінювати власні властивості об’єкта;
не робить автоматично незмінними вкладені об’єкти;
не захищає інші об’єкти від зміни їхніх прототипів.
Глибоке заморожування потребує окремої реалізації й має враховувати циклічні посилання, спеціальні об’єкти та продуктивність. Не слід бездумно заморожувати всі вбудовані прототипи: це може зламати бібліотеки, поліфіли або інструменти, які очікують можливість розширення.
У наступному прикладі:
використовується об’єкт без прототипу для словника;
власний клас задає структурну перевірку через Symbol.hasInstance;
властивості перевіряються через Object.hasOwn;
Object.prototype не змінюється.
class JsonSerializable {
static [Symbol.hasInstance](value) {
return (
value !== null &&
typeof value === "object" &&
typeof value.toJSON === "function"
);
}
}
function createCounter() {
const counts = Object.create(null);
return {
add(key) {
if (typeof key !== "string" || key.length === 0) {
throw new TypeError("Ключ має бути непорожнім рядком");
}
counts[key] = (counts[key] ?? 0) + 1;
},
get(key) {
return counts[key] ?? 0;
},
has(key) {
return Object.hasOwn(counts, key);
}
};
}
const counter = createCounter();
counter.add("javascript");
counter.add("javascript");
counter.add("prototype");
console.log(counter.get("javascript")); // 2
console.log(counter.has("prototype")); // true
console.log(Object.getPrototypeOf(counter)); // Object.prototype
const document = {
title: "Прототипи",
toJSON() {
return {
title: this.title
};
}
};
console.log(document instanceof JsonSerializable); // true
console.log({} instanceof JsonSerializable); // false
console.log(Object.hasOwn(document, "toJSON")); // trueObject.prototypeObject.prototype.isValid = function () {
return true;
};Це забруднює глобальну модель об’єктів. Використовуйте функцію, окремий модуль або власний клас.
hasOwnPropertyconst object = {
hasOwnProperty: "не метод"
};
object.hasOwnProperty("key"); // TypeErrorВикористовуйте:
Object.hasOwn(object, "key");in як перевірки власних данихif ("role" in user) {
// role може бути успадкованою властивістю.
}Використовуйте Object.hasOwn.
instanceof універсальною перевіркоюinstanceof може залежати від:
конкретного realm;
зміненого прототипного ланцюжка;
власного Symbol.hasInstance;
ручного підроблення прототипу.
Для масивів використовуйте Array.isArray, а для даних із зовнішніх джерел — явну валідацію структури.
thisconst set = new Set([1, 2]);
const has = set.has;
has(1); // TypeErrorБезпечні варіанти:
const has = set.has.bind(set);
console.log(has(1)); // trueабо:
console.log(set.has(1)); // true{} без урахування прототипуДля довільних ключів краще розглянути:
const dictionary = Object.create(null);А якщо потрібні саме ключі та значення без ризиків звичайних об’єктів, часто доречніший Map:
const dictionary = new Map();
dictionary.set("__proto__", "звичайний ключ");
console.log(dictionary.get("__proto__")); // звичайний ключВбудовані об’єкти JavaScript використовують прототипи для надання спільних методів і властивостей.
Object.getPrototypeOf дає змогу досліджувати ланцюжок прототипів.
Object.hasOwn перевіряє лише власні властивості й безпечніший за прямий виклик hasOwnProperty.
instanceof перевіряє прототипний ланцюжок, але його поведінку можна змінити через Symbol.hasInstance.
instanceof не завжди підходить для перевірки вбудованих типів, особливо між різними realm.
Розширення Object.prototype створює глобальні конфлікти та ускладнює підтримку системи.
Prototype pollution може дозволити ненадійним даним змінювати поведінку багатьох об’єктів.
Для словників із довільними ключами використовуйте Object.create(null) або Map.
Безпечніші альтернативи monkey patching — функції, модулі утиліт, власні класи та контрольовані символічні протоколи.