
When defining properties using constructor functions, the key is to focus on how JavaScript assigns these properties to each instance created by the constructor. Each time you invoke a constructor with new, you get a fresh object with its own copy of the properties you explicitly assign inside the function. For example:
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHello = function() {
console.log("Hi, I'm " + this.name + " and I'm " + this.age + " years old.");
};
}
const alice = new Person("Alice", 30);
const bob = new Person("Bob", 25);
alice.sayHello(); // Hi, I'm Alice and I'm 30 years old.
bob.sayHello(); // Hi, I'm Bob and I'm 25 years old.
What’s going on behind the scenes: this.name and this.age become properties of the object created by new Person. Because these assignments exist inside the constructor function, you get a uniquely configured object for each call. The downside here is when you define methods inside the constructor—like sayHello—you end up with a separate function instance for every object, which can be a memory and performance hit if you create many objects.
To mitigate this, you can combine constructor functions with the prototype to share methods among instances, while still defining properties inside the constructor:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function() {
console.log("Hi, I'm " + this.name + " and I'm " + this.age + " years old.");
};
const alice = new Person("Alice", 30);
const bob = new Person("Bob", 25);
alice.sayHello(); // Hi, I'm Alice and I'm 30 years old.
bob.sayHello(); // Hi, I'm Bob and I'm 25 years old.
This approach cleanly separates instance properties from shared behavior. The properties remain per-instance, but the methods live only once on the prototype chain. Internally, when you call alice.sayHello(), JavaScript looks on the alice object first, then its prototype, finds the method there, and runs it in the context of alice. This pattern is the bread and butter of classical JavaScript object creation before ES6 classes simplified syntax around it.
One subtle caveat: if you accidentally assign a property on the prototype—like Person.prototype.name = "default";—that won’t overwrite per-instance properties but will serve as a fallback if the instance lacks that property. This quirk can be useful but can also introduce bugs if misused. Keep instance data inside constructors and shared functionality on the prototype.
Another important detail is constructor-specific initializations that involve non-primitive or mutable objects. For example, if you assign an array or object as a property directly on the prototype, all instances share the same reference, resulting in unintentional cross-object mutations:
function Person(name) {
this.name = name;
}
Person.prototype.hobbies = []; // shared for all instances!
const alice = new Person("Alice");
const bob = new Person("Bob");
alice.hobbies.push("Reading");
console.log(bob.hobbies); // ["Reading"] - not what you'd want
The correct pattern is to initialize such mutable properties inside the constructor function so that each object gets its own copy:
function Person(name) {
this.name = name;
this.hobbies = [];
}
const alice = new Person("Alice");
const bob = new Person("Bob");
alice.hobbies.push("Reading");
console.log(bob.hobbies); // []
This distinction between methods on prototype and properties on the instance is fundamental to mastery of JavaScript object creation. When you start digging into performance and memory behavior, these patterns become more than academic—they can make or break the efficiency of your programs.
One last tip when defining properties inside constructors: if you want certain properties to be non-enumerable, or to have controlled writability and configurability, consider using Object.defineProperty instead of direct assignments. This allows you to tailor property descriptors:
function Person(name) {
Object.defineProperty(this, "name", {
value: name,
writable: true,
enumerable: false, // won't appear in for...in loops
configurable: false
});
}
const alice = new Person("Alice");
console.log(Object.keys(alice)); // []
console.log(alice.name); // Alice
Direct assignment is simpler and the norm, but for library authors or when you want stricter control over object layout, property descriptors are a powerful tool tucked inside the constructor function approach. It’s fine-grained control without spinning up classes or syntactic sugar, true to the JavaScript spirit.
This balance of constructor properties, prototype methods, and property descriptors gives you robust flexibility in defining your objects, which will allow you to optimize for clarity, reusability, or performance as your application demands. Next up:
Google Fitbit Air - Screenless Activity Tracker with Fitness, Heart Rate, and Sleep Tracking - Personalized AI-Powered Coaching - Up to 7 Days’ Battery Life - Works with iOS and Android - Lavender
Now retrieving the price.
(as of June 3, 2026 23:09 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Using class fields for concise property declarations
The introduction of class fields in ES2022 brought a more concise and readable way to declare properties directly within the class body, sidestepping the traditional verbosity of constructor initializations. Instead of explicitly assigning properties inside the constructor, you can now initialize them as part of the class definition like this:
class Person {
name = "";
age = 0;
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log(Hi, I'm ${this.name} and I'm ${this.age} years old.);
}
}
const alice = new Person("Alice", 30);
alice.sayHello(); // Hi, I'm Alice and I'm 30 years old.
Here, name and age are class fields, initialized with default values ("" and 0). These are syntactic sugar for assigning properties on this during instance creation, but with clearer intent. Notice that the constructor still overrides those defaults based on the parameters given.
Another powerful feature of class fields is the ability to assign properties outside of the constructor, and even use public fields without constructors at all, like so:
class Point {
x = 0;
y = 0;
move(dx, dy) {
this.x += dx;
this.y += dy;
}
}
const p = new Point();
console.log(p.x, p.y); // 0 0
p.move(5, 7);
console.log(p.x, p.y); // 5 7
In this example, x and y are implicitly set up for each instance without explicitly writing a constructor. The properties are created with those default values at instance initialization time. This can greatly reduce boilerplate when default property values suffice.
If you want to combine constructor parameters with class fields for custom initialization, you can use class fields without default values and assign inside the constructor:
class Rectangle {
width;
height;
constructor(width, height) {
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
const rect = new Rectangle(10, 20);
console.log(rect.area()); // 200
Note that uninitialized class fields like width and height are created per instance but hold undefined until assigned. This creates explicit declarations of instance state without defaulting to arbitrary values.
Class fields also seamlessly support arrow functions, useful for preserving this context in callbacks or event handlers without binding or explicitly storing context:
class Timer {
seconds = 0;
tick = () => {
this.seconds++;
console.log(this.seconds);
};
start() {
setInterval(this.tick, 1000);
}
}
const timer = new Timer();
timer.start();
// Logs: 1, 2, 3 ... every second
This arrow function method ensures this always points to the instance when used as a callback, which is a common pain point otherwise. Contrast this with defining tick as a normal method that needs explicit binding.
Under the hood, class fields are set up per instance, similarly to properties created in the constructor. If you want to see what this boils down to without class fields, here is an equivalent construction:
class Timer {
constructor() {
this.seconds = 0;
this.tick = () => {
this.seconds++;
console.log(this.seconds);
};
}
start() {
setInterval(this.tick, 1000);
}
}
That said, class fields bring cleaner syntax and better readability for declaring your instance state and capturing lexical this, moving the language closer to familiar class-based patterns in other languages while keeping JavaScript’s flexible prototypal essence.
Beware that class fields are still instance properties. They’re not part of the prototype, so defining methods or large data on class fields can have memory implications if duplicated unnecessarily per instance. The recommended pattern remains to put methods on the prototype (i.e., in class bodies as normal methods) and the stateful data on class fields or inside the constructor.
Also remember that while public class fields are now widely supported, private class fields (denoted by #fieldName) leverage a distinct internal slot and syntax that deserve their own deep dive. Their advantage is preventing outside access or modification, enforcing true encapsulation.
Next, we’ll explore how to use getters and setters in classes to control access, encapsulate validation, or derive properties dynamically without exposing explicit storage or requiring method calls. This pattern can highlight how abstraction can be implemented in JavaScript classes elegantly.
Using getters and setters for controlled access
Getters and setters in JavaScript provide a powerful mechanism for controlling access to object properties, which will allow you to implement encapsulation and validation without sacrificing the natural syntax of property access. By defining these accessors, you can execute custom logic whenever a property is accessed or modified, enhancing the robustness of your objects.
To create a getter or setter, you use the get and set keywords in your class definition. Here’s a simpler example using a class that manages a person’s age:
class Person {
#age;
constructor(name, age) {
this.name = name;
this.age = age; // Initial validation occurs here
}
get age() {
return this.#age;
}
set age(value) {
if (value < 0) {
console.error("Age cannot be negative.");
} else {
this.#age = value;
}
}
}
const alice = new Person("Alice", 30);
console.log(alice.age); // 30
alice.age = -5; // Error: Age cannot be negative.
console.log(alice.age); // Still 30
In this example, the age property is encapsulated using a private field #age. The getter allows read access to the age, while the setter includes validation logic that prevents negative ages. This encapsulation ensures that the internal state remains consistent and valid, which is a cornerstone of good object-oriented design.
Using getters and setters also allows you to derive properties dynamically. For instance, you might want to create a full name property that concatenates the first and last names:
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
get fullName() {
return ${this.firstName} ${this.lastName};
}
}
const bob = new Person("Bob", "Smith");
console.log(bob.fullName); // Bob Smith
The fullName getter provides a way to access a derived property without storing it explicitly. This can save memory and keep your object model clean, as the full name is calculated on-the-fly based on the existing state of the object.
Moreover, getters and setters can also be used in conjunction with other properties to implement computed properties that depend on multiple attributes. Here’s an example where we maintain a person’s height in centimeters and provide a computed property for height in meters:
class Person {
constructor(name, heightCm) {
this.name = name;
this.heightCm = heightCm;
}
get heightM() {
return this.heightCm / 100;
}
}
const charlie = new Person("Charlie", 180);
console.log(charlie.heightM); // 1.8
This pattern of using computed properties is particularly useful in scenarios where you want to keep the object state minimal while still providing rich, accessible interfaces for users of your class.
While using getters and setters can increase the encapsulation and flexibility of your objects, it’s important to strike a balance. Overusing them can lead to unexpected behaviors, especially if the logic inside gets too complex or if the properties are accessed frequently. Always consider performance implications when introducing additional logic in accessors.
Finally, it’s worth noting that getters and setters are also applicable to object literals, not just classes. If you want to create an object with similar behavior without using a class, you can define getters and setters within the object itself:
const person = {
firstName: "John",
lastName: "Doe",
get fullName() {
return ${this.firstName} ${this.lastName};
},
set fullName(value) {
[this.firstName, this.lastName] = value.split(" ");
}
};
console.log(person.fullName); // Albert Lee
person.fullName = "Jane Smith";
console.log(person.firstName); // Jane
console.log(person.lastName); // Smith
This syntax allows you to maintain similar encapsulation and validation principles even when not using class-based structures, providing flexibility in how you design your objects.
