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 totrue.enumerable: Whether the property appears infor-inloops. Defaults totrue.writable: Whether the property value can be changed. Defaults totrue.value: The actual data value. Defaults toundefined.
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 infor-inloops.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
returnstatement
Instantiation involves four steps:
- A new object is created
- The constructor's scope is assigned to the new object (
thisreferences it) - The constructor's code executes
- 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:
- Search the instance itself
- 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.