Skip to content
JavaScript js prototypes 4 min read

Object.create & Descriptors

Most objects in JavaScript inherit from Object.prototype, but sometimes you need precise control over an object’s prototype and the exact behavior of its properties. Object.create lets you build an object with a prototype you choose, and property descriptors let you control whether each property can be changed, listed, or deleted. Together they give you a low-level, declarative way to shape objects that constructors and object literals can’t fully express.

Creating objects with a chosen prototype

Object.create(proto) returns a brand-new object whose internal [[Prototype]] is the object you pass in. This is the cleanest way to set up inheritance without invoking a constructor.

const animal = {
  describe() {
    return `${this.name} is a ${this.type}`;
  },
};

const dog = Object.create(animal);
dog.name = "Rex";
dog.type = "dog";

console.log(dog.describe());
console.log(Object.getPrototypeOf(dog) === animal);

Output:

Rex is a dog
true

You can also create an object with no prototype at all by passing null. The result has no inherited methods like hasOwnProperty or toString, which makes it a safe, collision-free dictionary (often called a “bare” or “dict-mode” object).

const lookup = Object.create(null);
lookup.constructor = "this is just data, not Object's constructor";
console.log("constructor" in lookup);
console.log(Object.getPrototypeOf(lookup));

Output:

true
null

Use Object.create(null) for maps keyed by untrusted strings — it eliminates prototype-pollution surprises where keys like __proto__ or toString would otherwise collide with inherited members. For general key/value storage, a real Map is usually the better choice.

Property descriptors

Every property on an object is backed by a descriptor — an object that defines not just the value, but how the property behaves. There are two kinds: data descriptors and accessor descriptors.

A data descriptor has these attributes:

AttributeDefaultMeaning
valueundefinedThe value stored in the property.
writablefalseWhether the value can be reassigned with =.
enumerablefalseWhether the property shows up in for...in, Object.keys, and spread.
configurablefalseWhether the property can be deleted or its descriptor changed.

An accessor descriptor replaces value/writable with get and set functions, and still supports enumerable and configurable.

Defaults differ by API. Properties created with assignment or object literals default to writable: true, enumerable: true, configurable: true. But Object.defineProperty and the descriptor map of Object.create default every flag to false.

Object.defineProperty

Object.defineProperty(obj, key, descriptor) adds or modifies a single property with full control. This is how you create read-only fields, hidden internal state, or computed accessors.

const account = {};

Object.defineProperty(account, "id", {
  value: "ACC-9001",
  writable: false,
  enumerable: true,
  configurable: false,
});

Object.defineProperty(account, "balance", {
  value: 0,
  writable: true,
  enumerable: false, // hidden from Object.keys / JSON
});

account.id = "HACKED"; // silently ignored (throws in strict mode)
account.balance = 500;

console.log(account.id);
console.log(Object.keys(account));
console.log(account.balance);

Output:

ACC-9001
[ 'id' ]
500

For accessor properties, supply get and set instead of value:

const temperature = { _celsius: 20 };

Object.defineProperty(temperature, "fahrenheit", {
  get() {
    return this._celsius * 1.8 + 32;
  },
  set(f) {
    this._celsius = (f - 32) / 1.8;
  },
  enumerable: true,
});

console.log(temperature.fahrenheit);
temperature.fahrenheit = 212;
console.log(temperature._celsius);

Output:

68
100

Use Object.defineProperties(obj, descriptors) to define several at once.

Descriptors in Object.create

The second argument to Object.create is a descriptor map — the same shape Object.defineProperties accepts — letting you set both the prototype and the property behavior in one call.

const proto = {
  greet() {
    return `Hi, I'm ${this.name}`;
  },
};

const user = Object.create(proto, {
  name: { value: "Ada", enumerable: true, writable: true },
  role: { value: "admin", enumerable: false }, // hidden, read-only
});

console.log(user.greet());
console.log(Object.keys(user));
console.log(user.role);

Output:

Hi, I'm Ada
[ 'name' ]
admin

Inspecting descriptors

To read how a property is configured, use Object.getOwnPropertyDescriptor(obj, key), or Object.getOwnPropertyDescriptors(obj) for every own property at once. These are essential for debugging and for faithfully cloning objects (since spread copies values but not descriptor flags).

const config = {};
Object.defineProperty(config, "version", { value: "2.0", enumerable: true });

console.log(Object.getOwnPropertyDescriptor(config, "version"));

// Faithful clone that preserves writable/enumerable/configurable flags:
const clone = Object.create(
  Object.getPrototypeOf(config),
  Object.getOwnPropertyDescriptors(config)
);
console.log(clone.version);

Output:

{ value: '2.0', writable: false, enumerable: false, configurable: false }
const config = {};
2.0

The cloning idiom above is the recommended way to copy an object completely, because the spread operator ({ ...config }) only copies enumerable own properties and always resets their flags to true.

Best Practices

  • Reach for Object.create(proto) when you want clean prototypal inheritance without the ceremony of a constructor.
  • Use Object.create(null) for prototype-free dictionaries that won’t clash with inherited keys.
  • Prefer Object.defineProperty to expose read-only or non-enumerable fields rather than relying on naming conventions alone.
  • Remember that defineProperty and Object.create’s descriptor map default every flag to false — set enumerable/writable/configurable explicitly when you need them.
  • Clone objects with Object.create(proto, getOwnPropertyDescriptors(src)) when descriptor flags and accessors must be preserved.
  • Mark properties configurable: false only when you truly want them locked — it can’t be undone later.
  • Run in strict mode so writes to read-only properties throw loudly instead of failing silently.
Last updated June 1, 2026
Was this helpful?