Skip to main content

How to Use Prototype Methods and Create Objects Without __proto__ in JavaScript

Throughout the prototypes module, you have seen __proto__ used to get and set an object's prototype. While it works, __proto__ is a legacy feature with real problems: it can be exploited in security attacks, it does not work on all objects, and it blurs the line between data and prototype configuration. JavaScript provides a clean, modern API for working with prototypes that avoids all of these issues.

This guide covers the three core methods (Object.create, Object.getPrototypeOf, Object.setPrototypeOf), shows you how to create "very plain" dictionary objects with no prototype at all, and provides a complete reference for every prototype-related method in the language.

Object.create(proto, descriptors): Clean Prototype Setup

Object.create() is the most important method in the prototype API. It creates a new object with a specified prototype, optionally defining properties with full descriptor control. Unlike constructor functions or the class syntax, it gives you direct, explicit control over the prototype chain without any ceremony.

Basic Usage

const animal = {
eats: true,
walk() {
return `${this.name} walks`;
}
};

// Create a new object with animal as its prototype
const rabbit = Object.create(animal);
rabbit.name = "White Rabbit";
rabbit.jumps = true;

console.log(rabbit.name); // "White Rabbit" (own property)
console.log(rabbit.jumps); // true (own property)
console.log(rabbit.eats); // true (inherited from animal)
console.log(rabbit.walk()); // "White Rabbit walks" (inherited method, this = rabbit)

console.log(Object.getPrototypeOf(rabbit) === animal); // true

The first argument to Object.create() becomes the [[Prototype]] of the new object. The new object starts empty (no own properties) unless you add them afterward or use the second argument.

The Second Argument: Property Descriptors

The optional second argument accepts property descriptors in the same format as Object.defineProperties(). This lets you create an object with a prototype and fully configured properties in a single call:

const animal = {
eats: true,
walk() {
return `${this.name} walks`;
}
};

const rabbit = Object.create(animal, {
name: {
value: "White Rabbit",
writable: true,
enumerable: true,
configurable: true
},
jumps: {
value: true,
writable: false,
enumerable: true,
configurable: false
}
});

console.log(rabbit.name); // "White Rabbit"
console.log(rabbit.jumps); // true
console.log(rabbit.eats); // true (inherited)

// jumps is non-writable
rabbit.jumps = false; // Silent failure (TypeError in strict mode)
console.log(rabbit.jumps); // true (unchanged)

console.log(Object.getOwnPropertyDescriptor(rabbit, "jumps"));
// { value: true, writable: false, enumerable: true, configurable: false }

Creating a Complete Clone with Prototype Preservation

One of the most powerful uses of Object.create() is building a true clone of an object that preserves the prototype chain, property flags, getters/setters, non-enumerable properties, and Symbol keys:

const original = Object.create(
{ inherited: true },
{
visible: {
value: "I show up",
enumerable: true,
writable: true,
configurable: true
},
hidden: {
value: "I am non-enumerable",
enumerable: false,
writable: true,
configurable: true
}
}
);

// Shallow clone that preserves everything
const clone = Object.create(
Object.getPrototypeOf(original),
Object.getOwnPropertyDescriptors(original)
);

// Verify the clone
console.log(clone.visible); // "I show up"
console.log(clone.hidden); // "I am non-enumerable"
console.log(clone.inherited); // true (same prototype)

// Prototype is preserved
console.log(Object.getPrototypeOf(clone) === Object.getPrototypeOf(original)); // true

// Property flags are preserved
console.log(Object.getOwnPropertyDescriptor(clone, "hidden").enumerable); // false

// But they are different objects
console.log(clone === original); // false

Compare this with other cloning approaches that lose information:

const original = {};
Object.defineProperty(original, "secret", {
value: 42,
enumerable: false
});

// Spread operator: loses non-enumerable properties
const spreadClone = { ...original };
console.log(spreadClone.secret); // undefined (lost!)

// Object.assign: also loses non-enumerable properties
const assignClone = Object.assign({}, original);
console.log(assignClone.secret); // undefined (lost!)

// Object.create + getOwnPropertyDescriptors: preserves everything
const fullClone = Object.create(
Object.getPrototypeOf(original),
Object.getOwnPropertyDescriptors(original)
);
console.log(fullClone.secret); // 42 (preserved!)

Using Object.create() for Prototype-Based Inheritance

Object.create() provides the cleanest way to set up inheritance hierarchies without constructor functions:

// Base object
const vehicle = {
init(type, speed) {
this.type = type;
this.speed = speed;
return this;
},
describe() {
return `${this.type} going ${this.speed} km/h`;
}
};

// Child that inherits from vehicle
const car = Object.create(vehicle);
car.honk = function() {
return `${this.type}: Beep beep!`;
};

// Grandchild that inherits from car
const electricCar = Object.create(car);
electricCar.charge = function() {
return `${this.type} is charging`;
};

// Create instances by cloning and initializing
const tesla = Object.create(electricCar).init("Tesla", 120);

console.log(tesla.describe()); // "Tesla going 120 km/h" (from vehicle)
console.log(tesla.honk()); // "Tesla: Beep beep!" (from car)
console.log(tesla.charge()); // "Tesla is charging" (from electricCar)

// Full chain
console.log(Object.getPrototypeOf(tesla) === electricCar); // true
console.log(Object.getPrototypeOf(electricCar) === car); // true
console.log(Object.getPrototypeOf(car) === vehicle); // true
console.log(Object.getPrototypeOf(vehicle) === Object.prototype); // true

This is sometimes called the OLOO (Objects Linking to Other Objects) pattern, advocated as a simpler alternative to constructor functions and classes.

Object.getPrototypeOf() and Object.setPrototypeOf()

These two methods are the standard way to read and write an object's [[Prototype]].

Object.getPrototypeOf(obj)

Returns the [[Prototype]] of the given object:

const arr = [1, 2, 3];
const func = function() {};
const date = new Date();
const obj = {};

console.log(Object.getPrototypeOf(arr) === Array.prototype); // true
console.log(Object.getPrototypeOf(func) === Function.prototype); // true
console.log(Object.getPrototypeOf(date) === Date.prototype); // true
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype) === null); // true

It also works with objects created via Object.create():

const parent = { role: "parent" };
const child = Object.create(parent);

console.log(Object.getPrototypeOf(child) === parent); // true

// Object.create(null) creates a prototype-less object
const bare = Object.create(null);
console.log(Object.getPrototypeOf(bare)); // null

When called with a primitive, it returns the prototype of the corresponding wrapper type:

console.log(Object.getPrototypeOf(42) === Number.prototype);       // true (in non-strict)
console.log(Object.getPrototypeOf("hello") === String.prototype); // true
console.log(Object.getPrototypeOf(true) === Boolean.prototype); // true

Object.setPrototypeOf(obj, proto)

Changes the [[Prototype]] of an existing object:

const animal = {
speak() {
return `${this.name} speaks`;
}
};

const dog = { name: "Rex" };

// dog initially has Object.prototype
console.log(Object.getPrototypeOf(dog) === Object.prototype); // true
console.log(dog.speak); // undefined

// Change dog's prototype to animal
Object.setPrototypeOf(dog, animal);

console.log(Object.getPrototypeOf(dog) === animal); // true
console.log(dog.speak()); // "Rex speaks"

Why You Should Avoid Object.setPrototypeOf()

While Object.setPrototypeOf() is part of the standard and works correctly, it comes with a serious performance warning. Modern JavaScript engines optimize objects based on their structure, including their prototype. Changing the prototype after creation invalidates these optimizations and forces the engine to deoptimize code paths.

The MDN documentation explicitly warns:

Changing the [[Prototype]] of an object is, by the nature of how modern JavaScript engines optimize property accesses, currently a very slow operation in every browser and JavaScript engine.

// SLOW: changes prototype after creation
const obj = {};
Object.setPrototypeOf(obj, somePrototype);

// FAST: sets prototype at creation time
const obj = Object.create(somePrototype);
warning

Always prefer Object.create() to set prototypes at creation time. Use Object.setPrototypeOf() only in rare cases where you genuinely need to change an existing object's prototype at runtime. In years of JavaScript development, such cases are extremely rare.

Object.getPrototypeOf() vs. __proto__

These accomplish the same thing, but the modern API is safer and more predictable:

const parent = { x: 1 };
const child = Object.create(parent);

// These do the same thing
console.log(Object.getPrototypeOf(child) === parent); // true
console.log(child.__proto__ === parent); // true

// But __proto__ can be shadowed or missing
const dangerous = Object.create(null);
dangerous.__proto__ = parent; // This creates a DATA PROPERTY, not a prototype link!

console.log(Object.getPrototypeOf(dangerous)); // null (prototype is still null9
console.log(dangerous.__proto__ === parent); // true (but it's a regular property9
console.log(dangerous.x); // undefined (no inheritance happening9

This is a critical difference. On objects created with Object.create(null), __proto__ is just a regular string property name. It does not trigger any prototype mechanism. Object.getPrototypeOf() and Object.setPrototypeOf() always work correctly regardless of the object's structure.

"Very Plain" Objects: Object.create(null)

When you create an object with Object.create(null), you get an object with absolutely no prototype. No toString, no valueOf, no hasOwnProperty, no constructor, no __proto__ accessor. This is the "cleanest" object possible.

Why Would You Want This?

The primary use case is creating pure dictionaries (key-value stores) where keys come from untrusted sources, like user input or API responses. With a regular object, certain key names collide with inherited properties:

// Regular object: keys can collide with prototype properties
const settings = {};

settings["toString"] = "custom value";
settings["__proto__"] = "dangerous";
settings["constructor"] = "oops";

// Some of these break things
console.log(settings.toString); // "custom value" (shadows Object.prototype.toString)
console.log(typeof settings.toString); // "string" (no longer a function!)

// Trying to use it as a function throws
try {
settings.toString();
} catch (e) {
console.log(e.message); // settings.toString is not a function
}

With Object.create(null), there are no inherited properties to collide with:

// Very plain object (no prototype, no inherited properties)
const settings = Object.create(null);

settings["toString"] = "custom value";
settings["__proto__"] = "just a string";
settings["constructor"] = "totally safe";
settings["hasOwnProperty"] = "also safe";

console.log(settings["toString"]); // "custom value" (just data)
console.log(settings["__proto__"]); // "just a string" (just data)
console.log(settings["constructor"]); // "totally safe" (just data)
console.log(settings["hasOwnProperty"]); // "also safe" (just data)

// No prototype methods exist
console.log(Object.getPrototypeOf(settings)); // null

Prototype Pollution Prevention

Prototype pollution is a real security vulnerability where attackers inject properties into Object.prototype through user-controlled data. Object.create(null) objects are immune to __proto__-based attacks:

// Simulating user input (e.g., from JSON parsing)
const userInput = JSON.parse('{"__proto__": {"isAdmin": true}}');

// With a regular object, naive property copying can pollute Object.prototype
function unsafeMerge(target, source) {
for (const key in source) {
if (typeof source[key] === "object" && source[key] !== null) {
target[key] = target[key] || {};
unsafeMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}

// Using Object.create(null) as a safe storage
const safeStore = Object.create(null);
// Even if someone passes __proto__ as a key, it's just a regular property
safeStore["__proto__"] = "harmless string";
console.log(Object.getPrototypeOf(safeStore)); // null (unchanged)

Working with Very Plain Objects

Because very plain objects lack Object.prototype methods, you need to use static methods or borrowed methods when working with them:

const dict = Object.create(null);
dict.apple = 3;
dict.banana = 5;
dict.cherry = 2;

// These DON'T work on very plain objects
try {
dict.hasOwnProperty("apple");
} catch (e) {
console.log(e.message); // dict.hasOwnProperty is not a function
}

try {
dict.toString();
} catch (e) {
console.log(e.message); // dict.toString is not a function
}

// Use static methods instead
console.log(Object.keys(dict)); // ["apple", "banana", "cherry"]
console.log(Object.values(dict)); // [3, 5, 2]
console.log(Object.entries(dict)); // [["apple", 3], ["banana", 5], ["cherry", 2]]

// For hasOwnProperty, use Object.hasOwn (ES2022) or the borrowed method
console.log(Object.hasOwn(dict, "apple")); // true
console.log(Object.prototype.hasOwnProperty.call(dict, "apple")); // true

// For toString, borrow from Object.prototype
console.log(Object.prototype.toString.call(dict)); // "[object Object]"

// For JSON, it works fine
console.log(JSON.stringify(dict)); // '{"apple":3,"banana":5,"cherry":2}'

Iterating Over Very Plain Objects

Standard iteration methods work fine because they are static methods on Object, not instance methods:

const dict = Object.create(null);
dict.name = "Alice";
dict.age = 30;
dict.role = "developer";

// Object.keys, values, entries all work
for (const [key, value] of Object.entries(dict)) {
console.log(`${key}: ${value}`);
}
// name: Alice
// age: 30
// role: developer

// for...in also works (and there are no inherited properties to worry about!)
for (const key in dict) {
console.log(key);
}
// name
// age
// role
// No surprises (nothing inherited)

Notice an advantage here: when using for...in on a very plain object, you never need a hasOwnProperty check because there are no inherited enumerable properties.

Practical Example: A Safe Key-Value Cache

function createCache() {
const store = Object.create(null);

return {
get(key) {
return store[key];
},

set(key, value) {
store[key] = value;
},

has(key) {
return key in store;
},

delete(key) {
delete store[key];
},

keys() {
return Object.keys(store);
},

size() {
return Object.keys(store).length;
},

clear() {
for (const key of Object.keys(store)) {
delete store[key];
}
}
};
}

const cache = createCache();

// Any key is safe, including ones that match Object.prototype methods
cache.set("toString", "some value");
cache.set("constructor", "another value");
cache.set("__proto__", "yet another value");
cache.set("hasOwnProperty", "all safe");

console.log(cache.get("toString")); // "some value"
console.log(cache.get("__proto__")); // "yet another value"
console.log(cache.has("constructor")); // true
console.log(cache.size()); // 4
console.log(cache.keys());
// ["toString", "constructor", "__proto__", "hasOwnProperty"]

Very Plain Objects vs. Map

Both Object.create(null) and Map solve the key collision problem, but they serve different use cases:

// Object.create(null): when you need plain object compatibility
const dict = Object.create(null);
dict.key = "value";
console.log(JSON.stringify(dict)); // '{"key":"value"}' (works directly)

// Map: when you need non-string keys or guaranteed iteration order
const map = new Map();
map.set("key", "value");
map.set(42, "number key");
map.set({}, "object key");
console.log(JSON.stringify(Object.fromEntries(map)));
// '{"key":"value","42":"number key","[object Object]":"object key"}'
FeatureObject.create(null)Map
Key typesStrings and Symbols onlyAny type
JSON serializationDirect with JSON.stringifyNeeds Object.fromEntries
Performance (frequent add/delete)SlowerFaster
Prototype pollution safeYesYes
size trackingManual (Object.keys().length)Built-in (.size)
Iteration orderInsertion order (with caveats for integer keys)Strict insertion order
Memory overheadLowerHigher
tip

Use Object.create(null) when you need a simple dictionary with string keys that integrates well with JSON. Use Map when you need non-string keys, frequent additions and deletions, built-in size tracking, or guaranteed iteration order.

Complete Summary: Prototype Methods API

Here is the definitive reference for every method related to prototypes in JavaScript.

Creating Objects with Prototypes

// Object.create(proto[, descriptors])
// Creates a new object with the specified prototype and optional property descriptors

const proto = {
greet() {
return `Hello from ${this.name}`;
}
};

// Simplest form: just set the prototype
const obj1 = Object.create(proto);
obj1.name = "Object 1";

// With property descriptors
const obj2 = Object.create(proto, {
name: {
value: "Object 2",
writable: true,
enumerable: true,
configurable: true
},
id: {
value: 42,
writable: false,
enumerable: false,
configurable: false
}
});

// Create with null prototype (no inherited methods)
const bare = Object.create(null);

console.log(obj1.greet()); // "Hello from Object 1"
console.log(obj2.greet()); // "Hello from Object 2"
console.log(obj2.id); // 42

Reading Prototypes

// Object.getPrototypeOf(obj)
// Returns the [[Prototype]] of the given object

const child = Object.create(proto);

console.log(Object.getPrototypeOf(child) === proto); // true
console.log(Object.getPrototypeOf(proto) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype) === null); // true
console.log(Object.getPrototypeOf(Object.create(null)) === null); // true

Changing Prototypes (Use Sparingly)

// Object.setPrototypeOf(obj, proto)
// Changes the [[Prototype]] of an existing object (SLOW, avoid when possible)

const newProto = {
farewell() {
return `Goodbye from ${this.name}`;
}
};

const obj = { name: "Test" };
Object.setPrototypeOf(obj, newProto);

console.log(obj.farewell()); // "Goodbye from Test"

Checking Own Properties

// Object.hasOwn(obj, prop): ES2022, the modern standard
// obj.hasOwnProperty(prop): older, inherited from Object.prototype
// Object.prototype.hasOwnProperty.call(obj, prop): safe for all objects

const parent = { inherited: true };
const child = Object.create(parent);
child.own = true;

// Modern (recommended)
console.log(Object.hasOwn(child, "own")); // true
console.log(Object.hasOwn(child, "inherited")); // false

// Classic (works on most objects)
console.log(child.hasOwnProperty("own")); // true
console.log(child.hasOwnProperty("inherited")); // false

// Safe for any object (including Object.create(null))
const bare = Object.create(null);
bare.key = "value";
console.log(Object.hasOwn(bare, "key")); // true
console.log(Object.prototype.hasOwnProperty.call(bare, "key")); // true

Checking Prototype Relationships

// proto.isPrototypeOf(obj) is proto anywhere in obj's prototype chain?
// obj instanceof Constructor is Constructor.prototype in obj's prototype chain?

const animal = { eats: true };
const rabbit = Object.create(animal);
const babyRabbit = Object.create(rabbit);

console.log(animal.isPrototypeOf(rabbit)); // true
console.log(animal.isPrototypeOf(babyRabbit)); // true (transitive)
console.log(rabbit.isPrototypeOf(animal)); // false (not reverse)

// instanceof uses the constructor's prototype property
function Dog() {}
const rex = new Dog();
console.log(rex instanceof Dog); // true
console.log(rex instanceof Object); // true

Listing Properties

const parent = { inherited: true };
const child = Object.create(parent, {
own: {
value: "I am own",
enumerable: true,
writable: true,
configurable: true
},
hidden: {
value: "non-enumerable",
enumerable: false,
writable: true,
configurable: true
},
[Symbol("sym")]: {
value: "symbol-keyed",
enumerable: true,
writable: true,
configurable: true
}
});

// Only own enumerable string keys
console.log(Object.keys(child));
// ["own"]

// Only own string keys (including non-enumerable)
console.log(Object.getOwnPropertyNames(child));
// ["own", "hidden"]

// Only own Symbol keys
console.log(Object.getOwnPropertySymbols(child));
// [Symbol(sym)]

// ALL own keys (strings + symbols, enumerable + non-enumerable)
console.log(Reflect.ownKeys(child));
// ["own", "hidden", Symbol(sym)]

// for...in: own + inherited enumerable string keys
for (const key in child) {
console.log(key, "- own:", Object.hasOwn(child, key));
}
// "own" - own: true
// "inherited" - own: false

Getting and Setting Descriptors

// Single property
console.log(Object.getOwnPropertyDescriptor(child, "own"));
// { value: "I am own", writable: true, enumerable: true, configurable: true }

// All properties
console.log(Object.getOwnPropertyDescriptors(child));
// {
// own: { value: "I am own", writable: true, ... },
// hidden: { value: "non-enumerable", writable: true, enumerable: false, ... },
// [Symbol(sym)]: { value: "symbol-keyed", ... }
// }

// Define a single property
Object.defineProperty(child, "newProp", {
value: "new",
writable: true,
enumerable: true,
configurable: true
});

// Define multiple properties
Object.defineProperties(child, {
prop1: { value: 1, enumerable: true, writable: true, configurable: true },
prop2: { value: 2, enumerable: true, writable: true, configurable: true }
});

Complete Cloning Pattern

// The definitive way to create a shallow clone that preserves:
// - Prototype chain
// - Property flags (writable, enumerable, configurable)
// - Getters and setters
// - Non-enumerable properties
// - Symbol-keyed properties

function shallowClone(obj) {
return Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);
}

const original = Object.create({ inherited: true });
Object.defineProperty(original, "secret", {
value: 42,
enumerable: false
});
original.visible = "hello";

const clone = shallowClone(original);

console.log(clone.visible); // "hello"
console.log(clone.secret); // 42
console.log(clone.inherited); // true
console.log(clone === original); // false
console.log(Object.getPrototypeOf(clone) === Object.getPrototypeOf(original)); // true
console.log(Object.getOwnPropertyDescriptor(clone, "secret").enumerable); // false

Quick Reference Table

MethodPurposeNotes
Object.create(proto, desc)Create object with specified prototypePreferred for setting prototype
Object.getPrototypeOf(obj)Read [[Prototype]]Safe, works on all objects
Object.setPrototypeOf(obj, proto)Change [[Prototype]]Slow, avoid when possible
Object.hasOwn(obj, key)Check own propertyES2022, works on all objects
obj.hasOwnProperty(key)Check own propertyLegacy, fails on Object.create(null)
proto.isPrototypeOf(obj)Check if proto is in chainInstance method on the prototype
obj instanceof FCheck if F.prototype is in chainUses constructor's prototype
Object.keys(obj)Own enumerable string keysReturns array
Object.getOwnPropertyNames(obj)Own string keys (all)Includes non-enumerable
Object.getOwnPropertySymbols(obj)Own Symbol keysOnly symbols
Reflect.ownKeys(obj)All own keysStrings + Symbols
Object.getOwnPropertyDescriptor(obj, key)One property's descriptorReturns descriptor object
Object.getOwnPropertyDescriptors(obj)All descriptorsFor complete cloning
Object.defineProperty(obj, key, desc)Define/modify one propertyFull flag control
Object.defineProperties(obj, descs)Define/modify many propertiesFull flag control
Object.preventExtensions(obj)Block new propertiesWeakest lock
Object.seal(obj)Block new/delete, non-configurableMedium lock
Object.freeze(obj)Block all changesStrongest lock (shallow)
Object.isExtensible(obj)Check extensibilityReturns boolean
Object.isSealed(obj)Check if sealedReturns boolean
Object.isFrozen(obj)Check if frozenReturns boolean

The __proto__ Escape Hatch

For completeness, here is why __proto__ still exists and when you might see it:

// __proto__ in object literals (the ONLY acceptable modern use)
// It's part of the specification for object literal syntax
const child = {
__proto__: parentObject,
ownProp: "value"
};

// This is equivalent to:
const child2 = Object.create(parentObject);
child2.ownProp = "value";

Using __proto__ inside an object literal is actually specified behavior (not just a legacy accessor) and is considered acceptable by many style guides. Outside of object literals, always use the Object.create/getPrototypeOf/setPrototypeOf API.

// These are NOT recommended
obj.__proto__ = newProto; // Use Object.setPrototypeOf instead
const proto = obj.__proto__; // Use Object.getPrototypeOf instead
caution

The key takeaway from this entire module: use Object.create() to set prototypes at creation time, Object.getPrototypeOf() to read them, and avoid changing prototypes after creation. For pure dictionaries with user-controlled keys, use Object.create(null) or Map. Never rely on __proto__ for prototype manipulation outside of object literals.