Чистий об'єкт без prototype
Чистий об'єкт (його ще називають object dictionary) - це об'єкт без прототипа, тобто без Object.prototype і всіх його вбудованих методів на кшталт toString чи hasOwnProperty. Такий об'єкт чудово підходить для чистих сховищ даних, словників і мап, де треба бути впевненим, що жодна вбудована властивість не перетнеться з вашими ключами.
Теорія
TL;DR
- Чистий об'єкт створюється викликом
Object.create(null). - У нього
Object.getPrototypeOf(obj)дорівнюєnull, ланцюжка прототипів немає взагалі. - Він не має ні
toString, ніhasOwnProperty, ні спеціальної поведінки__proto__. - Тому ключі
toStringабо__proto__стають звичайними ключами і нічого не ламають. - Перевіряти наявність властивості треба через
Object.prototype.hasOwnProperty.call(obj, key)абоObject.hasOwn(obj, key). - Сучасна альтернатива для словників, це
Map.
Швидкий приклад
Найпростіший і правильний спосіб:
const obj = Object.create(null);Це створює об'єкт без прототипа: obj.__proto__ дорівнює undefined, а Object.getPrototypeOf(obj) дорівнює null.
Перевіримо:
const obj = Object.create(null);
console.log(Object.getPrototypeOf(obj)); // null
console.log('__proto__' in obj); // false
console.log(typeof obj.toString); // "undefined"У такого об'єкта немає ланцюжка успадкування: він не успадковує навіть від Object.prototype.
Чому це корисно
Звичайні об'єкти успадковують купу вбудованих методів:
const normal = {};
console.log(Object.getPrototypeOf(normal) === Object.prototype); // true
// Ці методи доступні
console.log(normal.hasOwnProperty); // [Function]
console.log(normal.toString); // [Function]Це може бути небезпечно, якщо ключі перетинаються з цими іменами:
const map = {};
map.toString = 'value'; // може зламати код, якщо хтось викличе map.toString()Чистий об'єкт знімає цю проблему:
const map = Object.create(null);
map.toString = 'value'; // просто ключ
console.log(map.toString); // "value"Приклад використання: словники й мапи
const dict = Object.create(null);
dict.apple = 1;
dict.banana = 2;
dict['__proto__'] = 3; // безпечно, не впливає на прототип
console.log(dict.apple); // 1
console.log(dict['__proto__']); // 3У звичайних об'єктах __proto__ має особливу поведінку (це аксесор з Object.prototype), а тут це звичайна властивість. Саме тому чисті об'єкти часто використовують для кешів і для розбору даних, що прийшли ззовні: вони природно захищені від prototype pollution.
Недолік: немає вбудованих методів
const obj = Object.create(null);
obj.name = 'Alice';
console.log(obj.hasOwnProperty('name')); // TypeError: obj.hasOwnProperty is not a functionЯкщо треба перевірити наявність властивості, скористайтеся безпечним способом:
console.log(Object.prototype.hasOwnProperty.call(obj, 'name')); // true
console.log(Object.hasOwn(obj, 'name')); // true, сучасний варіантТе саме стосується toString(): такий об'єкт не можна просто конкатенувати з рядком, а console.log у деяких середовищах показує його як [Object: null prototype].
Альтернатива: використовувати Map
Сучасний спосіб зберігати словники:
const map = new Map();
map.set('apple', 1);
map.set('__proto__', 2);
console.log(map.get('__proto__')); // 2Map розв'язує ту саму задачу, але дозволяє будь-які ключі, зокрема об'єкти, зберігає порядок вставки і має властивість size.
Підсумок
| Спосіб | Прототип | Доступ до вбудованих методів | Підходить для словників |
|---|---|---|---|
{} | Object.prototype | є (toString, hasOwnProperty) | ризиковано |
Object.create(null) | null | немає | так |
new Map() | не звичайний об'єкт | методи set, get, has | так, сучасний варіант |
Щоб створити чистий об'єкт без
Object.prototype, використовуйтеconst obj = Object.create(null): він не успадковує нічого і чудово підходить для безпечних словників.
Типові помилки
- Викликати
obj.hasOwnProperty(key)на чистому об'єкті. ОтримаєтеTypeError, бо методу просто немає. ВикористовуйтеObject.hasOwn(obj, key). - Плутати
Object.create(null)з{}. Другий варіант успадковує відObject.prototypeз усіма наслідками. - Передавати чистий об'єкт у код, який очікує звичайний. Бібліотека, яка всередині викликає
value.toString(), впаде. - Думати, що
JSON.stringifyне працює. Він працює нормально, проблеми виникають саме з неявним приведенням до рядка. - Використовувати чистий об'єкт там, де краще підійде
Map. Якщо ключі не рядки або потрібен розмір і порядок, берітьMap. - Забувати, що об'єкт з
JSON.parseзвичайний. Розбір зовнішнього JSON дає об'єкт з прототипом, тому перевіряйте ключ__proto__перед злиттям.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.