Object-Oriented Programming in JavaScript

JavaScript differs from class-based OOP languages. While languages like Java or C++ use classes as blueprints for creating objects, ECMAScript defines JavaScript objects as unordered collections of properties. Each property has a name that maps to a value, which can be a primitive, object, or function.

Object Creation

Using the Object Constructor

var employee = new Object();
employee.name = 'Alice';
employee.department = 28;
employee.getDetails = function() {
    return this.name;
};

Object Literal Notation

var employee = {
    name: 'Alice',
    department: 28,
    getDetails: function() {
        return this.name;
    }
};

Properties can be added dynamically using the dot operator, and removed using delete or by setting the value to undefined:

employee.bonus = 5000;
alert(employee.bonus);
delete employee.department;
alert(employee.department);

Property Attributes

ECMAScript 5 defines property descriptors that control how properties behave. These attributes are internal and not directly accessible from JavaScript code.

Data Properties have four attributes:

  • configurable: Whether the property can be deleted or redefined. Defaults to true.
  • enumerable: Whether the property appears in for-in loops. Defaults to true.
  • writable: Whether the property value can be changed. Defaults to true.
  • value: The actual data value. Defaults to undefined.

The Object.defineProperty() method modifies these attributes:

var user = {};
Object.defineProperty(user, 'username', {
    configurable: false,
    writable: false,
    value: 'Robin'
});
alert(user.username);
delete user.username;
user.username = 'Sarah';
alert(user.username);

Attempting to delete or modify username has no effect due to the descriptor settings. Once configurable is set to false, it cannot be changed back to true.

Accessor Properties use getter and setter functions instead of a data value:

  • configurable: Whether the property can be deleted or redefined.
  • enumerable: Whether the property appears in for-in loops.
  • get: Function called when reading the property.
  • set: Function called when writing to the property.

Accessor properties require Object.defineProperty() for definition:

var user = {
    birthYear: 1995
};
Object.defineProperty(user, 'adultStatus', {
    get: function() {
        var currentYear = new Date().getFullYear();
        return currentYear - this.birthYear >= 18;
    }
});
alert(user.adultStatus);

The getter and setter functions are optional, but you cannot set configurable or writable when defining accessor properties.

For defining multiple properties at once, use Object.defineProperties():

var user = {};
Object.defineProperties(user, {
    firstName: { value: 'Robin' },
    lastName: { value: 'Chen' },
    fullName: {
        get: function() {
            return this.firstName + ' ' + this.lastName;
        }
    }
});
alert(user.fullName);

To inspect property descriptors, use Object.getOwnPropertyDescriptor():

var desc = Object.getOwnPropertyDescriptor(user, 'firstName');
alert(desc.value);

Object Creation Patterns

Factory Pattern

function createEmployee(name, role, salary) {
    var obj = new Object();
    obj.name = name;
    obj.role = role;
    obj.salary = salary;
    obj.getInfo = function() {
        return this.name;
    };
    return obj;
}
var emp = createEmployee('Alice', 'Developer', 85000);

This pattern simplifies object creation with parameters, but all objects are created using the native Object constructor, so the resulting type cannot be determined.

Constructor Pattern

function Employee(name, role, salary) {
    this.name = name;
    this.role = role;
    this.salary = salary;
    this.getInfo = function() {
        return this.name;
    };
}
var emp1 = new Employee('Alice', 'Developer', 85000);
var emp2 = new Employee('Bob', 'Designer', 72000);

Custom constructors define a specific type with properties and methods. Unlike the factory pattern:

  • No explicit object creation
  • Properties are assigned directly to this
  • No return statement

Instantiation involves four steps:

  1. A new object is created
  2. The constructor's scope is assigned to the new object (this references it)
  3. The constructor's code executes
  4. The object is returned

Instances can be verified with instanceof:

alert(emp1 instanceof Employee);
alert(emp1 instanceof Object);
alert(emp1.constructor === emp2.constructor);

A drawback emerges when methods are defined in the constructor—each instance gets its own function instance:

function Employee(name, role, salary) {
    this.name = name;
    this.getInfo = new Function() {
        return this.name;
    };
}

This creates redundant function instances across objects. Separating methods to external functions solves this but breaks encapsulation:

function Employee(name, role, salary) {
    this.name = name;
    this.getInfo = displayInfo;
}
function displayInfo() {
    return this.name;
}

Prototype Pattern

Every function has a prototype property that points to an object containing shared properties and methods. All instances created with new share this prototype object:

function Employee() {}
Employee.prototype.name = 'Alice';
Employee.prototype.role = 'Developer';
Employee.prototype.getInfo = function() {
    return this.name;
};
var emp1 = new Employee();
alert(emp1.getInfo());
var emp2 = new Employee();
alert(emp1.getInfo === emp2.getInfo);

The prototype is a reference to the prototype object, while each instance has an internal [[prototype]] link to it.

Property lookup follows this sequence:

  1. Search the instance itself
  2. If not found, search the prototype object

Instance properties shadow prototype properties:

var emp1 = new Employee();
emp1.name = 'Bob';
alert(emp1.name);
var emp2 = new Employee();
alert(emp2.name);

Use hasOwnProperty() to distinguish between instance and prototype properties:

alert(emp1.hasOwnProperty('name'));
alert(Employee.prototype.hasOwnProperty('name'));

The in operator finds properties in both instance and prototype:

alert('name' in emp1);

Prtootype modification affects all objects:

String.prototype.trimWhitespace = function() {
    return this.replace(/^\s+/, '').replace(/\s+$/, '');
};
var text = '   leading and trailing spaces   ';
alert('!' + text.trimWhitespace() + '!');

The prototype pattern has limitations. Constructor parameters cannot be passed, and reference-type properties are shared across instances:

function Employee() {}
Employee.prototype.name = 'Alice';
Employee.prototype.skills = ['JavaScript', 'CSS'];
var emp1 = new Employee();
emp1.skills.push('HTML');
var emp2 = new Employee();
alert(emp2.skills);

Modifying emp1.skills also changes emp2.skills because they reference the same array.

Constructor and Prototype Combination

This pattern uses the constructor for instance-specific properties and the prototype for shared methods:

function Employee(name, role, salary) {
    this.name = name;
    this.role = role;
    this.salary = salary;
    this.skills = ['JavaScript', 'CSS'];
}
Employee.prototype = {
    constructor: Employee,
    getInfo: function() {
        return this.name;
    },
    getRole: function() {
        return this.role;
    }
};
var emp1 = new Employee('Alice', 'Developer', 85000);
emp1.skills.push('TypeScript');
var emp2 = new Employee('Bob', 'Designer', 72000);
alert(emp1.skills);
alert(emp2.skills);
alert(emp1.getInfo === emp2.getInfo);

Each instance has its own skills array while sharing getInfo and getRole methods. This approach supports parameter passing and maximizes memory efficiency.

Dynamic Prototype Pattern

This pattern encapsulates all initialization logic within the constructor while maintaining the constructor-prototype separation:

function Employee(name, role, salary) {
    this.name = name;
    this.role = role;
    this.salary = salary;
    this.skills = ['JavaScript', 'CSS'];
    if (typeof this.getInfo !== 'function') {
        Employee.prototype = {
            constructor: Employee,
            getInfo: function() {
                return this.name;
            },
            getRole: function() {
                return this.role;
            }
        };
    }
}
var emp1 = new Employee('Alice', 'Developer', 85000);
emp1.skills.push('TypeScript');
var emp2 = new Employee('Bob', 'Designer', 72000);
alert(emp1.skills);
alert(emp2.skills);
alert(emp1.getInfo === emp2.getInfo);

The initialization code runs only once—subsequent instantiations skip the prototype definition. Any modifications to the prototype immediately affect all existnig instances.

Tags: javascript object-oriented Prototype constructor design-patterns

Posted on Tue, 04 Aug 2026 16:46:40 +0000 by lorri