javascript

examples

examples.js
/**
 * =====================================================
 * 7.3 PROPERTY DESCRIPTORS - EXAMPLES
 * =====================================================
 * Comprehensive examples of property descriptors in JavaScript
 */

// =====================================================
// EXAMPLE 1: Getting Property Descriptors
// =====================================================
console.log('=== Example 1: Getting Property Descriptors ===');

const person = {
  name: 'Alice',
  age: 30,
};

// Get descriptor for a single property
const nameDescriptor = Object.getOwnPropertyDescriptor(person, 'name');
console.log('Name descriptor:', nameDescriptor);
/*
{
    value: "Alice",
    writable: true,
    enumerable: true,
    configurable: true
}
*/

// Get all descriptors
const allDescriptors = Object.getOwnPropertyDescriptors(person);
console.log('All descriptors:', allDescriptors);

// =====================================================
// EXAMPLE 2: Defining Properties with defineProperty
// =====================================================
console.log('\n=== Example 2: Defining Properties ===');

const config = {};

// Define a read-only property
Object.defineProperty(config, 'VERSION', {
  value: '1.0.0',
  writable: false,
  enumerable: true,
  configurable: false,
});

console.log('VERSION:', config.VERSION); // "1.0.0"

// Try to change it (silently fails in non-strict mode)
config.VERSION = '2.0.0';
console.log('After attempted change:', config.VERSION); // Still "1.0.0"

// =====================================================
// EXAMPLE 3: Default Descriptor Values
// =====================================================
console.log('\n=== Example 3: Default Descriptor Values ===');

const obj = {};

// Object literal assignment - all defaults are true
obj.normalProp = 'I am normal';
console.log(
  'Normal property:',
  Object.getOwnPropertyDescriptor(obj, 'normalProp')
);
// { value: "I am normal", writable: true, enumerable: true, configurable: true }

// defineProperty - all defaults are FALSE!
Object.defineProperty(obj, 'definedProp', {
  value: 'I am defined',
  // writable, enumerable, configurable all default to false!
});
console.log(
  'Defined property:',
  Object.getOwnPropertyDescriptor(obj, 'definedProp')
);
// { value: "I am defined", writable: false, enumerable: false, configurable: false }

// =====================================================
// EXAMPLE 4: Writable Attribute
// =====================================================
console.log('\n=== Example 4: Writable Attribute ===');

const constants = {};

Object.defineProperty(constants, 'PI', {
  value: 3.14159265359,
  writable: false,
  enumerable: true,
  configurable: false,
});

Object.defineProperty(constants, 'E', {
  value: 2.71828182845,
  writable: false,
  enumerable: true,
  configurable: false,
});

console.log('PI:', constants.PI);
console.log('E:', constants.E);

// Cannot modify
constants.PI = 3;
console.log('PI after attempted change:', constants.PI); // Still 3.14159...

// =====================================================
// EXAMPLE 5: Enumerable Attribute
// =====================================================
console.log('\n=== Example 5: Enumerable Attribute ===');

const user = {
  name: 'John',
  email: 'john@example.com',
};

// Add a non-enumerable property
Object.defineProperty(user, '_internalId', {
  value: 'usr_12345',
  enumerable: false,
  writable: false,
  configurable: false,
});

Object.defineProperty(user, '_createdAt', {
  value: new Date().toISOString(),
  enumerable: false,
});

// Regular enumeration - won't show non-enumerable props
console.log('Object.keys:', Object.keys(user));
// ["name", "email"]

console.log('Object.values:', Object.values(user));
// ["John", "john@example.com"]

console.log('for...in loop:');
for (const key in user) {
  console.log(`  ${key}: ${user[key]}`);
}
// name: John
// email: john@example.com

console.log('JSON.stringify:', JSON.stringify(user));
// {"name":"John","email":"john@example.com"}

// But we can still access them directly
console.log('Direct access to _internalId:', user._internalId);
// "usr_12345"

// Get ALL properties including non-enumerable
console.log('getOwnPropertyNames:', Object.getOwnPropertyNames(user));
// ["name", "email", "_internalId", "_createdAt"]

// =====================================================
// EXAMPLE 6: Configurable Attribute
// =====================================================
console.log('\n=== Example 6: Configurable Attribute ===');

const secure = {};

// Configurable: true - can be modified or deleted
Object.defineProperty(secure, 'tempData', {
  value: 'temporary',
  writable: true,
  enumerable: true,
  configurable: true,
});

// Configurable: false - locked down
Object.defineProperty(secure, 'permanentId', {
  value: 'perm_123',
  writable: false,
  enumerable: true,
  configurable: false,
});

// Can modify tempData's configuration
Object.defineProperty(secure, 'tempData', {
  enumerable: false, // Works - configurable is true
});

// Can delete tempData
delete secure.tempData;
console.log('After delete tempData:', secure.tempData); // undefined

// Cannot modify permanentId's configuration
try {
  Object.defineProperty(secure, 'permanentId', {
    enumerable: false, // Fails - configurable is false
  });
} catch (e) {
  console.log('Cannot reconfigure:', e.message);
}

// Cannot delete permanentId
const deleted = delete secure.permanentId;
console.log('Delete permanentId result:', deleted); // false
console.log('permanentId still exists:', secure.permanentId); // "perm_123"

// =====================================================
// EXAMPLE 7: Accessor Properties (Getters and Setters)
// =====================================================
console.log('\n=== Example 7: Accessor Properties ===');

const temperature = {
  _celsius: 0,
};

Object.defineProperty(temperature, 'celsius', {
  get() {
    return this._celsius;
  },
  set(value) {
    if (typeof value !== 'number') {
      throw new TypeError('Temperature must be a number');
    }
    this._celsius = value;
  },
  enumerable: true,
  configurable: true,
});

Object.defineProperty(temperature, 'fahrenheit', {
  get() {
    return (this._celsius * 9) / 5 + 32;
  },
  set(value) {
    this._celsius = ((value - 32) * 5) / 9;
  },
  enumerable: true,
  configurable: true,
});

Object.defineProperty(temperature, 'kelvin', {
  get() {
    return this._celsius + 273.15;
  },
  set(value) {
    this._celsius = value - 273.15;
  },
  enumerable: true,
  configurable: true,
});

// Use the accessors
temperature.celsius = 25;
console.log(`Celsius: ${temperature.celsius}°C`);
console.log(`Fahrenheit: ${temperature.fahrenheit}°F`);
console.log(`Kelvin: ${temperature.kelvin}K`);

// Set via Fahrenheit
temperature.fahrenheit = 212;
console.log(`\nAfter setting to 212°F:`);
console.log(`Celsius: ${temperature.celsius}°C`); // 100

// =====================================================
// EXAMPLE 8: Getter-Only Properties (Read-Only)
// =====================================================
console.log('\n=== Example 8: Getter-Only Properties ===');

const circle = {
  _radius: 5,
};

Object.defineProperty(circle, 'radius', {
  get() {
    return this._radius;
  },
  set(value) {
    if (value <= 0) throw new RangeError('Radius must be positive');
    this._radius = value;
  },
  enumerable: true,
});

// Read-only computed properties
Object.defineProperty(circle, 'diameter', {
  get() {
    return this._radius * 2;
  },
  enumerable: true,
});

Object.defineProperty(circle, 'circumference', {
  get() {
    return 2 * Math.PI * this._radius;
  },
  enumerable: true,
});

Object.defineProperty(circle, 'area', {
  get() {
    return Math.PI * this._radius ** 2;
  },
  enumerable: true,
});

console.log('Radius:', circle.radius);
console.log('Diameter:', circle.diameter);
console.log('Circumference:', circle.circumference.toFixed(2));
console.log('Area:', circle.area.toFixed(2));

// Try to set diameter (no setter, will fail silently)
circle.diameter = 20;
console.log('Diameter after attempted set:', circle.diameter); // Still 10

// =====================================================
// EXAMPLE 9: Object.defineProperties (Multiple)
// =====================================================
console.log('\n=== Example 9: Object.defineProperties ===');

const product = {};

Object.defineProperties(product, {
  _id: {
    value: 'prod_001',
    writable: false,
    enumerable: false,
    configurable: false,
  },
  _price: {
    value: 0,
    writable: true,
    enumerable: false,
    configurable: false,
  },
  name: {
    value: 'Widget',
    writable: true,
    enumerable: true,
    configurable: true,
  },
  price: {
    get() {
      return `$${this._price.toFixed(2)}`;
    },
    set(value) {
      if (value < 0) throw new RangeError('Price cannot be negative');
      this._price = value;
    },
    enumerable: true,
    configurable: true,
  },
  id: {
    get() {
      return this._id;
    },
    enumerable: true,
    configurable: false,
  },
});

product.price = 99.99;
console.log('Product:', product);
console.log('ID:', product.id);
console.log('Price:', product.price);
console.log('Enumerable keys:', Object.keys(product));

// =====================================================
// EXAMPLE 10: Object.freeze()
// =====================================================
console.log('\n=== Example 10: Object.freeze() ===');

const frozenConfig = Object.freeze({
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3,
});

console.log('Is frozen:', Object.isFrozen(frozenConfig)); // true

// All these fail silently (or throw in strict mode)
frozenConfig.apiUrl = 'http://new-url.com'; // Cannot change
frozenConfig.newProp = 'value'; // Cannot add
delete frozenConfig.timeout; // Cannot delete

console.log('frozenConfig after attempts:', frozenConfig);
// Still { apiUrl: "https://api.example.com", timeout: 5000, retries: 3 }

// Check property descriptors
console.log(
  'apiUrl descriptor:',
  Object.getOwnPropertyDescriptor(frozenConfig, 'apiUrl')
);
// writable: false, configurable: false

// =====================================================
// EXAMPLE 11: Object.seal()
// =====================================================
console.log('\n=== Example 11: Object.seal() ===');

const sealedUser = Object.seal({
  name: 'Alice',
  age: 30,
  role: 'admin',
});

console.log('Is sealed:', Object.isSealed(sealedUser)); // true

// CAN change existing values
sealedUser.name = 'Bob';
sealedUser.age = 31;
console.log('After changes:', sealedUser); // { name: "Bob", age: 31, role: "admin" }

// CANNOT add new properties
sealedUser.email = 'bob@example.com';
console.log('Has email:', 'email' in sealedUser); // false

// CANNOT delete properties
delete sealedUser.role;
console.log('Has role:', 'role' in sealedUser); // true

// =====================================================
// EXAMPLE 12: Object.preventExtensions()
// =====================================================
console.log('\n=== Example 12: Object.preventExtensions() ===');

const limited = Object.preventExtensions({
  x: 1,
  y: 2,
});

console.log('Is extensible:', Object.isExtensible(limited)); // false

// CAN modify existing properties
limited.x = 100;
console.log('x:', limited.x); // 100

// CAN delete properties
delete limited.y;
console.log('y:', limited.y); // undefined

// CANNOT add new properties
limited.z = 3;
console.log('z:', limited.z); // undefined

// =====================================================
// EXAMPLE 13: Deep Freeze
// =====================================================
console.log('\n=== Example 13: Deep Freeze ===');

function deepFreeze(obj) {
  // Freeze properties before freezing self
  Object.getOwnPropertyNames(obj).forEach((name) => {
    const prop = obj[name];
    if (prop && typeof prop === 'object') {
      deepFreeze(prop);
    }
  });
  return Object.freeze(obj);
}

const shallowFrozen = Object.freeze({
  level1: 'frozen',
  nested: {
    level2: 'not frozen with Object.freeze',
  },
});

// Nested object can be modified!
shallowFrozen.nested.level2 = 'CHANGED!';
console.log('Shallow frozen nested:', shallowFrozen.nested.level2); // "CHANGED!"

// Deep freeze example
const deepFrozenObj = deepFreeze({
  level1: 'frozen',
  nested: {
    level2: 'also frozen',
    deeper: {
      level3: 'frozen too',
    },
  },
});

deepFrozenObj.nested.level2 = 'try to change';
console.log('Deep frozen nested:', deepFrozenObj.nested.level2); // "also frozen"

// =====================================================
// EXAMPLE 14: Creating Immutable Objects with Proxies
// =====================================================
console.log('\n=== Example 14: Immutable Objects with Proxies ===');

function createImmutable(obj) {
  return new Proxy(obj, {
    set(target, property, value) {
      throw new TypeError(
        `Cannot modify property '${property}' of immutable object`
      );
    },
    deleteProperty(target, property) {
      throw new TypeError(
        `Cannot delete property '${property}' of immutable object`
      );
    },
    get(target, property) {
      const value = target[property];
      // Recursively wrap nested objects
      if (value && typeof value === 'object') {
        return createImmutable(value);
      }
      return value;
    },
  });
}

const immutableData = createImmutable({
  user: {
    name: 'Alice',
    settings: {
      theme: 'dark',
    },
  },
});

try {
  immutableData.user.name = 'Bob';
} catch (e) {
  console.log('Caught error:', e.message);
}

console.log('Name still:', immutableData.user.name); // "Alice"

// =====================================================
// EXAMPLE 15: Lazy Property Initialization
// =====================================================
console.log('\n=== Example 15: Lazy Property Initialization ===');

function createLazyObject() {
  const obj = {};

  Object.defineProperty(obj, 'expensiveData', {
    get() {
      console.log('Computing expensive data...');
      const data = Array.from({ length: 5 }, (_, i) => i * i);

      // Replace getter with computed value (self-modifying)
      Object.defineProperty(obj, 'expensiveData', {
        value: data,
        writable: false,
        enumerable: true,
        configurable: false,
      });

      return data;
    },
    enumerable: true,
    configurable: true,
  });

  return obj;
}

const lazy = createLazyObject();
console.log('Object created');

// First access - computes value
console.log('First access:', lazy.expensiveData);
// "Computing expensive data..." then [0, 1, 4, 9, 16]

// Second access - uses cached value
console.log('Second access:', lazy.expensiveData);
// [0, 1, 4, 9, 16] (no "Computing" message)

// =====================================================
// EXAMPLE 16: Property Validation with Setters
// =====================================================
console.log('\n=== Example 16: Property Validation ===');

function createValidatedUser() {
  const user = {
    _email: '',
    _age: 0,
  };

  Object.defineProperties(user, {
    email: {
      get() {
        return this._email;
      },
      set(value) {
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        if (!emailRegex.test(value)) {
          throw new TypeError(`Invalid email: ${value}`);
        }
        this._email = value;
      },
      enumerable: true,
    },
    age: {
      get() {
        return this._age;
      },
      set(value) {
        if (!Number.isInteger(value)) {
          throw new TypeError('Age must be an integer');
        }
        if (value < 0 || value > 150) {
          throw new RangeError('Age must be between 0 and 150');
        }
        this._age = value;
      },
      enumerable: true,
    },
  });

  return user;
}

const validatedUser = createValidatedUser();

// Valid values
validatedUser.email = 'alice@example.com';
validatedUser.age = 30;
console.log('Valid user:', {
  email: validatedUser.email,
  age: validatedUser.age,
});

// Invalid values
try {
  validatedUser.email = 'not-an-email';
} catch (e) {
  console.log('Email validation:', e.message);
}

try {
  validatedUser.age = 200;
} catch (e) {
  console.log('Age validation:', e.message);
}

// =====================================================
// EXAMPLE 17: Creating Constants Object
// =====================================================
console.log('\n=== Example 17: Creating Constants Object ===');

function createConstants(constantsObj) {
  const constants = {};

  for (const [key, value] of Object.entries(constantsObj)) {
    Object.defineProperty(constants, key, {
      value: value,
      writable: false,
      enumerable: true,
      configurable: false,
    });
  }

  return Object.freeze(constants);
}

const HTTP_STATUS = createConstants({
  OK: 200,
  CREATED: 201,
  BAD_REQUEST: 400,
  UNAUTHORIZED: 401,
  NOT_FOUND: 404,
  SERVER_ERROR: 500,
});

console.log('HTTP_STATUS:', HTTP_STATUS);
console.log('OK:', HTTP_STATUS.OK);

// Cannot modify
HTTP_STATUS.OK = 999;
console.log('OK after attempted change:', HTTP_STATUS.OK); // Still 200

// =====================================================
// EXAMPLE 18: Private Properties Pattern
// =====================================================
console.log('\n=== Example 18: Private Properties Pattern ===');

function createCounter(initialValue = 0) {
  const counter = {};
  let count = initialValue;

  Object.defineProperties(counter, {
    value: {
      get() {
        return count;
      },
      enumerable: true,
    },
    increment: {
      value() {
        count++;
        return this;
      },
      enumerable: false,
    },
    decrement: {
      value() {
        count--;
        return this;
      },
      enumerable: false,
    },
    reset: {
      value() {
        count = initialValue;
        return this;
      },
      enumerable: false,
    },
  });

  return Object.freeze(counter);
}

const counter = createCounter(10);
console.log('Initial value:', counter.value);
counter.increment().increment().increment();
console.log('After 3 increments:', counter.value);
counter.reset();
console.log('After reset:', counter.value);

// Methods are hidden from enumeration
console.log('Visible properties:', Object.keys(counter));

// =====================================================
// EXAMPLE 19: Observable Properties
// =====================================================
console.log('\n=== Example 19: Observable Properties ===');

function createObservable(initial) {
  const listeners = {};
  const data = { ...initial };

  const observable = {};

  for (const key of Object.keys(initial)) {
    Object.defineProperty(observable, key, {
      get() {
        return data[key];
      },
      set(newValue) {
        const oldValue = data[key];
        if (oldValue !== newValue) {
          data[key] = newValue;
          if (listeners[key]) {
            listeners[key].forEach((fn) => fn(newValue, oldValue, key));
          }
        }
      },
      enumerable: true,
    });
  }

  // Add subscribe method (non-enumerable)
  Object.defineProperty(observable, 'subscribe', {
    value(property, callback) {
      if (!listeners[property]) {
        listeners[property] = [];
      }
      listeners[property].push(callback);

      // Return unsubscribe function
      return () => {
        listeners[property] = listeners[property].filter(
          (fn) => fn !== callback
        );
      };
    },
  });

  return observable;
}

const state = createObservable({ count: 0, name: 'Initial' });

// Subscribe to changes
const unsubscribe = state.subscribe('count', (newVal, oldVal) => {
  console.log(`count changed: ${oldVal} -> ${newVal}`);
});

state.count = 1; // "count changed: 0 -> 1"
state.count = 2; // "count changed: 1 -> 2"

unsubscribe();
state.count = 3; // No log (unsubscribed)
console.log('Final count:', state.count);

// =====================================================
// EXAMPLE 20: Copying Objects with Descriptors
// =====================================================
console.log('\n=== Example 20: Copying Objects with Descriptors ===');

const original = {
  visible: "I'm enumerable",
};

Object.defineProperty(original, 'hidden', {
  value: "I'm not enumerable",
  enumerable: false,
});

Object.defineProperty(original, 'computed', {
  get() {
    return "I'm a getter";
  },
  enumerable: true,
});

// Object.assign loses descriptors
const shallowCopy = Object.assign({}, original);
console.log('Shallow copy keys:', Object.keys(shallowCopy));
console.log(
  'Shallow copy descriptors:',
  Object.getOwnPropertyDescriptors(shallowCopy)
);

// Proper copy with descriptors
const properCopy = Object.create(
  Object.getPrototypeOf(original),
  Object.getOwnPropertyDescriptors(original)
);

console.log('\nProper copy keys:', Object.keys(properCopy));
console.log(
  'Proper copy descriptors:',
  Object.getOwnPropertyDescriptors(properCopy)
);
console.log('Hidden property preserved:', properCopy.hidden);
console.log('Getter works:', properCopy.computed);

console.log('\n=== All Examples Complete ===');
Examples - JavaScript Tutorial | DeepML