Skip to content
JavaScript Basics: Control Object Property Behavior

JavaScript Basics: Control Object Property Behavior

October 18, 2026

An object property has more than a value. Its descriptor can control whether assignment changes it, whether enumeration includes it, and whether it can be reconfigured. Learning those switches helps explain objects that behave differently from ordinary object literals. Use descriptors sparingly when simpler properties would do.

These examples were tested on Linux with Node.js v24.19.0. They use built-in APIs only, need no network access, and include assertions that check the results. See the ECMAScript Object specification for the API contract.

Prepare a scratch directory

You need Bash, a text editor, Node.js 24 on your PATH, and the mktemp, rm, and rmdir utilities. Keep using the same terminal. The guarded commands stop the shell if setup fails. Save each named source file as UTF-8; .mjs selects ES modules.

node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

The tested runtime prints:

v24.19.0

1. Inspect ordinary property defaults

Save this as inspect-property-flags.mjs:

import assert from 'node:assert/strict';

const settings = { theme: 'dark' };
const descriptor = Object.getOwnPropertyDescriptor(settings, 'theme');
assert.deepEqual(descriptor, {
  value: 'dark', writable: true, enumerable: true, configurable: true,
});
console.log(JSON.stringify(descriptor));

Run it:

node inspect-property-flags.mjs

Output:

{"value":"dark","writable":true,"enumerable":true,"configurable":true}

A normal object-literal data property is writable, enumerable, and configurable. Writable controls assignment to its value. Enumerable controls participation in operations such as Object.keys(). Configurable controls deletion and most descriptor changes. getOwnPropertyDescriptor() inspects an object’s own property, rather than walking its prototype chain; it returns undefined when that own property does not exist.

2. Define a fixed identifier

Save this as define-fixed-identifier.mjs:

import assert from 'node:assert/strict';

const record = {};
Object.defineProperty(record, 'id', { value: 'item-7', enumerable: true });
assert.throws(() => { record.id = 'item-8'; }, TypeError);
assert.throws(() => { delete record.id; }, TypeError);
assert.deepEqual(Object.keys(record), ['id']);
assert.equal(record.id, 'item-7');
console.log(`Identifier: ${record.id}`);
console.log('Assignment and deletion rejected');

Run it:

node define-fixed-identifier.mjs

Output:

Identifier: item-7
Assignment and deletion rejected

For a newly defined property, omitted writable, enumerable, and configurable flags default to false. Here only enumeration is enabled. ES modules run in strict mode, so writing to or deleting this protected property throws. In non-strict code, comparable attempts can fail silently. This protection is shallow: a non-writable property holding an object would still allow changes inside that object unless those are separately restricted.

3. Expose a calculated value

Save this as define-calculated-total.mjs:

import assert from 'node:assert/strict';

const line = { quantity: 3, unitPrice: 10 };
Object.defineProperty(line, 'total', {
  enumerable: true,
  configurable: true,
  get() { return this.quantity * this.unitPrice; },
});
assert.equal(line.total, 30);
line.quantity = 4;
assert.equal(line.total, 40);
assert.throws(() => { line.total = 99; }, TypeError);
console.log(`Calculated total: ${line.total}`);
console.log(JSON.stringify(line));

Run it:

node define-calculated-total.mjs

Output:

Calculated total: 40
{"quantity":4,"unitPrice":10,"total":40}

An accessor descriptor uses get and optionally set, rather than value and writable. The getter runs when the property is read; it does not store a second total that can become outdated. With no setter, assignment throws in this module. Because the property is enumerable, JSON serialization reads it too. Keep getters predictable and inexpensive. A non-enumerable property is merely omitted by some operations, not hidden or secured against inspection.

Clean up

In the same terminal, remove only the example files, return to your original directory, and remove the now-empty scratch directory:

rm -- inspect-property-flags.mjs define-fixed-identifier.mjs define-calculated-total.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

Acknowledgements

This post was created with assistance from AI.

Last updated on