We use cookies to enhance your experience on the site
CodeWorlds

Object Properties and Methods

In Jurassic Park, every piece of equipment has specific parameters - some are read-only (like the model serial number), some change dynamically (like the current battery level), and some are calculated based on other values. JavaScript objects can model exactly this: read-only properties, computed properties, hidden internals with controlled access.

Property Descriptors

Every object property has a descriptor - a set of attributes that control how the property behaves:

1const sensor = { value: 42 };
2
3// Get property descriptor
4const descriptor = Object.getOwnPropertyDescriptor(sensor, 'value');
5console.log(descriptor);
6// {
7//   value: 42,
8//   writable: true,    // can be changed
9//   enumerable: true,  // shows up in for...in
10//   configurable: true // can be deleted/reconfigured
11// }

defineProperty

Object.defineProperty
lets you precisely control a property:

1const dino = { name: 'Rex' };
2
3// Read-only property (writable: false)
4Object.defineProperty(dino, 'id', {
5  value: 'DINO-001',
6  writable: false,    // cannot be changed
7  enumerable: true,
8  configurable: false // cannot be deleted
9});
10
11console.log(dino.id); // "DINO-001"
12dino.id = 'OTHER';    // silently fails (or throws in strict mode)
13console.log(dino.id); // "DINO-001" - unchanged
14
15// Attempt to delete won't work either
16delete dino.id; // returns false
17console.log(dino.id); // still "DINO-001"

defineProperties

You can define multiple properties at once with

Object.defineProperties
:

1const triceratops = {};
2
3Object.defineProperties(triceratops, {
4  species: {
5    value: "Triceratops",
6    writable: false
7  },
8  diet: {
9    value: "herbivore",
10    writable: false
11  },
12  age: {
13    value: 5,
14    writable: true // Age can change
15  }
16});
17
18console.log(triceratops.species); // "Triceratops"
19console.log(triceratops.diet); // "herbivore"
20console.log(triceratops.age); // 5
21
22triceratops.age = 6;
23console.log(triceratops.age); // 6

Hidden Properties

1const dino = { name: 'Rex' };
2
3// Hidden property (enumerable: false)
4Object.defineProperty(dino, '_internalCode', {
5  value: 'SECRET-XYZ',
6  writable: true,
7  enumerable: false,   // won't show up in for...in or Object.keys
8  configurable: false
9});
10
11console.log(Object.keys(dino)); // ['name', 'id'] - _internalCode hidden!
12console.log(dino._internalCode); // "SECRET-XYZ" - still accessible directly

Getters and Setters

Getters and setters let you control property access - execute code when reading or writing:

1class DinosaurHealth {
2  constructor(name) {
3    this.name = name;
4    this._health = 100;  // _health is the private backing store
5    this._hunger = 0;
6  }
7
8  // Getter - called when you read .health
9  get health() {
10    return this._health;
11  }
12
13  // Setter - called when you write .health = value
14  set health(value) {
15    if (value < 0) value = 0;
16    if (value > 100) value = 100;
17    this._health = value;
18
19    if (value < 20) {
20      console.log(`WARNING: ${this.name} critically ill!`);
21    }
22  }
23
24  // Computed getter
25  get status() {
26    if (this._health > 80) return 'Excellent';
27    if (this._health > 50) return 'Good';
28    if (this._health > 20) return 'Poor';
29    return 'Critical';
30  }
31
32  get hunger() {
33    return this._hunger;
34  }
35
36  set hunger(value) {
37    this._hunger = Math.max(0, Math.min(100, value));
38  }
39}
40
41const rex = new DinosaurHealth('Rex');
42rex.health = 150;  // setter clamps to 100
43console.log(rex.health);  // 100
44console.log(rex.status);  // "Excellent"
45
46rex.health = 15;   // WARNING: Rex critically ill!
47console.log(rex.status);  // "Critical"

You can also define getters and setters using

Object.defineProperty()
:

1const dinosaur = {
2  _name: "Rex",
3  _species: "Tyrannosaurus"
4};
5
6Object.defineProperty(dinosaur, "fullName", {
7  get: function() {
8    return `${this._name} the ${this._species}`;
9  },
10
11  set: function(value) {
12    // Assuming value has format "Name the Species"
13    const parts = value.split(" the ");
14    if (parts.length === 2) {
15      this._name = parts[0];
16      this._species = parts[1];
17    } else {
18      console.log("Invalid format. Use: 'Name the Species'");
19    }
20  },
21
22  enumerable: true,
23  configurable: true
24});
25
26console.log(dinosaur.fullName); // "Rex the Tyrannosaurus"
27dinosaur.fullName = "Blue the Velociraptor";
28console.log(dinosaur._name); // "Blue"
29console.log(dinosaur._species); // "Velociraptor"

Binding this - bind, call, apply

The

this
keyword in methods refers to the object the method is called on. However, in some cases (e.g., when passing a method as a callback) we can lose the
this
context. The
bind
method allows explicit setting of the
this
value.

1const rex = {
2  name: "Rex",
3  makeSound: function() {
4    console.log(`${this.name} roars loudly!`);
5  }
6};
7
8// Normal usage
9rex.makeSound(); // "Rex roars loudly!"
10
11// Problematic usage - lost this context
12const rexSound = rex.makeSound;
13// rexSound(); // Error: this.name is undefined
14
15// Solution - use bind to set the this value
16const boundRexSound = rex.makeSound.bind(rex);
17boundRexSound(); // "Rex roars loudly!"
18
19// We can also bind this to a different object
20const blue = { name: "Blue" };
21const blueSound = rex.makeSound.bind(blue);
22blueSound(); // "Blue roars loudly!"

call and apply

call
and
apply
let you invoke a function with a specific
this
value and arguments:

1function attack(target, damage) {
2  console.log(`${this.name} attacks ${target} and deals ${damage} damage!`);
3}
4
5const trex = { name: "Tyrannosaurus Rex" };
6const raptor = { name: "Velociraptor" };
7
8// call with explicitly passed arguments
9attack.call(trex, "Triceratops", 50);
10// "Tyrannosaurus Rex attacks Triceratops and deals 50 damage!"
11
12// apply with arguments in an array
13attack.apply(raptor, ["Gallimimus", 30]);
14// "Velociraptor attacks Gallimimus and deals 30 damage!"

Object.freeze, Object.seal, Object.preventExtensions

These methods lock down object mutability at different levels:

1// preventExtensions - no new properties, but existing ones can change
2const openDino = { name: 'Rex', health: 100 };
3Object.preventExtensions(openDino);
4
5openDino.health = 90;    // OK
6openDino.newProp = 'x';  // silently ignored
7console.log(openDino.newProp); // undefined
8
9// seal - no new properties, no deletions, but values can change
10const sealedDino = { name: 'Blue', health: 100 };
11Object.seal(sealedDino);
12
13sealedDino.health = 80;  // OK - value can change
14sealedDino.zone = 'A';   // ignored - can't add
15delete sealedDino.name;  // ignored - can't delete
16console.log(sealedDino); // { name: 'Blue', health: 80 }
17
18// freeze - completely immutable (shallow!)
19const frozenDino = Object.freeze({ name: 'Stego', stats: { health: 100 } });
20frozenDino.name = 'Other'; // ignored
21frozenDino.stats.health = 50; // WORKS! freeze is shallow
22console.log(frozenDino.name);         // "Stego" - protected
23console.log(frozenDino.stats.health); // 50 - nested object NOT frozen!

Symbol Keys

Symbols as property keys are unique and hidden from standard enumeration:

1const SECRET_ID = Symbol('secretId');
2const VERSION = Symbol('version');
3
4const dino = {
5  name: 'Rex',
6  [SECRET_ID]: 'DINO-007',
7  [VERSION]: '2.0'
8};
9
10console.log(dino.name);         // "Rex" - regular property
11console.log(dino[SECRET_ID]);   // "DINO-007" - symbol property
12console.log(Object.keys(dino)); // ['name'] - symbols not listed!
13
14// Getting symbol keys
15const symbolKeys = Object.getOwnPropertySymbols(dino);
16console.log(symbolKeys); // [Symbol(secretId), Symbol(version)]

Well-Known Symbols

JavaScript has built-in symbols that customize object behavior:

1class DinosaurCollection {
2  constructor() {
3    this.dinos = ['Rex', 'Blue', 'Delta'];
4  }
5
6  // Make the object iterable with for...of
7  [Symbol.iterator]() {
8    let index = 0;
9    const dinos = this.dinos;
10    return {
11      next() {
12        if (index < dinos.length) {
13          return { value: dinos[index++], done: false };
14        }
15        return { value: undefined, done: true };
16      }
17    };
18  }
19
20  // Customize string representation
21  get [Symbol.toStringTag]() {
22    return 'DinosaurCollection';
23  }
24}
25
26const collection = new DinosaurCollection();
27for (const dino of collection) {
28  console.log(dino); // "Rex", "Blue", "Delta"
29}
30
31console.log([...collection]); // ["Rex", "Blue", "Delta"]

Proxy

Proxy
lets you intercept and customize fundamental object operations:

1const dinoData = {
2  name: 'Rex',
3  health: 100,
4  zone: 'A'
5};
6
7const monitoredDino = new Proxy(dinoData, {
8  get(target, prop) {
9    console.log(`Reading: ${prop}`);
10    return target[prop];
11  },
12
13  set(target, prop, value) {
14    console.log(`Setting ${prop} = ${value} (was: ${target[prop]})`);
15    if (prop === 'health' && (value < 0 || value > 100)) {
16      throw new RangeError(`health must be 0-100, got ${value}`);
17    }
18    target[prop] = value;
19    return true;
20  }
21});
22
23console.log(monitoredDino.name); // "Reading: name" then "Rex"
24monitoredDino.health = 85;       // "Setting health = 85 (was: 100)"
25// monitoredDino.health = 200;   // RangeError!

Dinosaur Health Monitor

1class DinosaurHealthMonitor {
2  constructor(dinoData) {
3    this._data = { ...dinoData };
4    this._log = [];
5
6    // Use Proxy to intercept all property changes
7    return new Proxy(this, {
8      set(target, prop, value) {
9        if (prop.startsWith('_')) {
10          // Private properties - set directly
11          target[prop] = value;
12          return true;
13        }
14
15        const old = target._data[prop];
16        target._data[prop] = value;
17        target._log.push({
18          prop,
19          from: old,
20          to: value,
21          time: new Date().toISOString()
22        });
23        return true;
24      },
25
26      get(target, prop) {
27        if (prop.startsWith('_') || typeof target[prop] === 'function') {
28          return target[prop];
29        }
30        return target._data[prop];
31      }
32    });
33  }
34
35  getLog() {
36    return this._log;
37  }
38
39  getReport() {
40    return { ...this._data };
41  }
42}
43
44const monitor = new DinosaurHealthMonitor({
45  name: 'Rex',
46  health: 100,
47  hunger: 0
48});
49
50monitor.health = 85;
51monitor.hunger = 40;
52monitor.health = 70;
53
54console.log(monitor.getLog());
55// [{ prop: 'health', from: 100, to: 85, ... }, ...]
56console.log(monitor.getReport());
57// { name: 'Rex', health: 70, hunger: 40 }

Summary

In this lesson we explored advanced properties and methods of JavaScript objects:

  1. Property descriptors - allow precise control over property attributes
  2. Getters and setters - enable executing code when reading or writing properties
  3. Binding this - bind, call, and apply methods help control function context
  4. Symbols - provide unique identifiers for object properties
  5. Object freezing - freeze, seal, and preventExtensions methods restrict object mutability
  6. Proxy - allows intercepting and customizing fundamental object operations

These techniques are essential when building complex systems like our dinosaur health monitoring system in Jurassic Park. They enable more reliable, secure, and elegant programming solutions.

"Object properties in JavaScript are like the control panel in an enclosure" - says Dr. Rex. "Some switches are locked (read-only), some have warning lights (setters), some are behind glass (non-enumerable). Proxy is like a camera that records every button press!"

Go to CodeWorlds