In short
JavaScript object constructors, getters and setters give you more control over objects. Learn constructor functions with new, get and set, Object.defineProperty, and Object.freeze.
JavaScript object constructors, getters and setters give you more control over objects. Learn constructor functions with new, get and set, Object.defineProperty, and Object.freeze.
Constructor functions and the new keyword
When you need many similar objects, like products in a shop, writing each object literal by hand gets repetitive. Before classes existed, JavaScript developers used constructor functions: normal functions that are meant to be called with the new keyword to build objects. By convention their names start with a capital letter.
function Product(name, price) {
this.name = name;
this.price = price;
this.inStock = true;
}
const mug = new Product("Coffee mug", 12);
const lamp = new Product("Desk lamp", 35);
console.log(mug.name, mug.price); // Output: Coffee mug 12
console.log(lamp); // Output: Product { name: 'Desk lamp', price: 35, inStock: true }
console.log(mug instanceof Product); // Output: true
When you write new Product(...), JavaScript does four things for you:
- Creates a brand-new empty object.
- Links that object to
Product.prototype, so it can share methods. - Runs the function with
thispointing to the new object, which fills in the properties. - Returns the new object automatically.
Methods were added to the prototype, so all products share one copy instead of each object getting its own:
Product.prototype.label = function () {
return `${this.name} - $${this.price.toFixed(2)}`;
};
console.log(mug.label()); // Output: Coffee mug - $12.00
console.log(lamp.label()); // Output: Desk lamp - $35.00
Common mistake: Forgetting
new. CallingProduct("Pen", 2)without it runs the function as a plain call. In strict mode,thisisundefinedand you get aTypeError; in old sloppy mode, it silently creates global variables callednameandprice. Classes refuse to be called withoutnew, which prevents this bug.
Why classes replaced constructor functions
The class syntax, which you will study in the Classes lesson, does exactly the same job with clearer code. Under the hood, a class still creates a constructor function and a prototype. Here is the same product as a class:
class ProductItem {
constructor(name, price) {
this.name = name;
this.price = price;
this.inStock = true;
}
label() {
return `${this.name} - $${this.price.toFixed(2)}`;
}
}
const book = new ProductItem("Notebook", 4.5);
console.log(book.label()); // Output: Notebook - $4.50
// ProductItem("Pen", 2); // TypeError: Class constructor ProductItem cannot be invoked without 'new'
Everything is in one block, methods go on the prototype automatically, forgetting new throws a clear error, and the body is always in strict mode. Write classes in new code. You still need to recognise constructor functions, because you will see them in older code and libraries.
Getters and setters in object literals
A getter is a property that runs a function when you read it. A setter runs a function when you assign to it. From the outside they look like ordinary properties: no brackets needed. You write them with the get and set keywords, even in a plain object literal:
const user = {
firstName: "Amara",
lastName: "Okafor",
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
set fullName(value) {
const [first, last] = value.split(" ");
this.firstName = first;
this.lastName = last;
},
};
console.log(user.fullName); // Output: Amara Okafor (no brackets!)
user.fullName = "Leo Martin"; // runs the setter
console.log(user.firstName); // Output: Leo
console.log(user.lastName); // Output: Martin
The getter builds fullName from the other two properties every time, so it is always up to date. The setter does the opposite: it splits the new value and stores the parts.
Setters are also great for validation, which means checking a value before you accept it:
const thermostat = {
_celsius: 20, // the underscore says "please don't touch this directly"
get celsius() {
return this._celsius;
},
set celsius(value) {
if (typeof value !== "number" || value < 5 || value > 30) {
console.log(`Ignored ${value}: choose 5 to 30 degrees`);
return;
}
this._celsius = value;
},
get fahrenheit() {
return this._celsius * 9 / 5 + 32;
},
};
thermostat.celsius = 25;
console.log(thermostat.fahrenheit); // Output: 77
thermostat.celsius = 90; // Output: Ignored 90: choose 5 to 30 degrees
console.log(thermostat.celsius); // Output: 25
fahrenheit has only a getter, so it is a read-only calculated value.
Object.defineProperty basics
Every property has hidden settings called property descriptors. Object.defineProperty(object, name, descriptor) lets you set them yourself:
| Setting | Meaning | Default with defineProperty |
|---|---|---|
value | The property's value | undefined |
writable | Can the value be changed? | false |
enumerable | Does it show up in for...in, Object.keys and JSON.stringify? | false |
configurable | Can the property be deleted or its settings changed? | false |
"use strict";
const order = { total: 59.9 };
Object.defineProperty(order, "id", {
value: "ORD-1042",
writable: false, // read-only
enumerable: false, // hidden from loops and JSON
});
console.log(order.id); // Output: ORD-1042
console.log(Object.keys(order)); // Output: [ 'total' ]
console.log(JSON.stringify(order)); // Output: {"total":59.9}
try {
order.id = "HACKED";
} catch (error) {
console.log(error.name); // Output: TypeError
}
Normal properties you create with = or in a literal have all three settings set to true. With defineProperty, anything you leave out becomes false, so be explicit. You will not use it every day, but it explains how built-in objects keep some properties read-only and hidden.
Object.freeze, Object.seal and Object.preventExtensions
Sometimes you want to protect a whole object, like a settings object that should never change while the app runs. JavaScript gives you three levels of protection:
| Method | Add new properties? | Delete properties? | Change existing values? | Check with |
|---|---|---|---|---|
Object.preventExtensions(obj) | No | Yes | Yes | Object.isExtensible(obj) |
Object.seal(obj) | No | No | Yes | Object.isSealed(obj) |
Object.freeze(obj) | No | No | No | Object.isFrozen(obj) |
const config = Object.freeze({
apiUrl: "https://api.example.com",
theme: { color: "blue" },
});
config.apiUrl = "https://evil.example.com"; // ignored (TypeError in strict mode)
config.debug = true; // ignored
console.log(config.apiUrl); // Output: https://api.example.com
console.log(Object.isFrozen(config)); // Output: true
config.theme.color = "red"; // this works!
console.log(config.theme.color); // Output: red
That last line shows the important catch: Object.freeze is shallow. It freezes only the top level. The nested theme object is a separate object, and it is not frozen. To freeze everything, you can freeze each nested object too, using recursion:
function deepFreeze(obj) {
for (const value of Object.values(obj)) {
if (typeof value === "object" && value !== null) {
deepFreeze(value); // freeze nested objects and arrays first
}
}
return Object.freeze(obj);
}
const settings = deepFreeze({ theme: { color: "blue" }, sizes: [12, 14] });
settings.theme.color = "red"; // ignored now (TypeError in strict mode)
console.log(settings.theme.color); // Output: blue
settings.sizes.push(16); // TypeError: Cannot add property 2, object is not extensible
Note:
constandObject.freezeprotect different things.conststops the variable from pointing to a different object.Object.freezestops the object itself from changing. For a truly fixed settings object, use both.
Practice exercise
Create a bankAccount object with a _balance of 100, a getter balance, and a setter balance that refuses negative numbers with a console message. Add a getter summary that returns a string like Balance: $100.00. Then create const rates = Object.freeze({ savings: 0.03, nested: { bonus: 0.01 } }), try to change both savings and nested.bonus, and log which one changed.
Hint: use toFixed(2) in the summary getter. Only the nested value changes, because Object.freeze is shallow.
Key takeaways
- Constructor functions build objects with
new; methods go on the prototype. Modern code uses classes instead. - Getters (
get) run when you read a property; setters (set) run when you assign one, which is perfect for validation. Object.definePropertycontrols a property'swritable,enumerableandconfigurablesettings.preventExtensions,sealandfreezegive three levels of protection, andfreezeis shallow.