Objects & Prototypes
Understand JavaScript objects, the 'this' keyword, prototype chain, and Object.create for inheritance.
Object Literals & Shorthand
ES6 introduced several shorthand syntaxes for object literals that reduce boilerplate when creating objects. Property shorthand lets you omit the value when the variable name matches the property name. Method shorthand removes the need for the 'function' keyword when defining methods on objects. Computed property names allow you to use dynamic expressions as property keys by wrapping them in square brackets.
// Property shorthand
const name = "Alice";
const age = 30;
const user = { name, age }; // { name: "Alice", age: 30 }
// Method shorthand
const calculator = {
value: 0,
add(n) {
this.value += n;
return this;
},
subtract(n) {
this.value -= n;
return this;
},
result() {
return this.value;
},
};
console.log(calculator.add(10).subtract(3).result()); // 7
// Computed property names
const field = "email";
const profile = {
[field]: "alice@example.com",
[`${field}Verified`]: true,
};
console.log(profile); // { email: "alice@example.com", emailVerified: true }
// Property enumeration
const config = { host: "localhost", port: 3000, debug: true };
console.log(Object.keys(config)); // ["host", "port", "debug"]
console.log(Object.values(config)); // ["localhost", 3000, true]
console.log(Object.entries(config)); // [["host","localhost"],...]
The 'this' Keyword
The 'this' keyword in JavaScript refers to the object that is executing the current function, but its value depends on how the function is called, not where it is defined. In a method call, 'this' refers to the object before the dot. In a regular function call, 'this' is the global object (or undefined in strict mode). Arrow functions do not have their own 'this' and instead inherit it from the enclosing scope. The bind, call, and apply methods allow you to explicitly set the value of 'this'.
// 'this' in methods
const person = {
name: "Alice",
greet() {
console.log(`Hi, I'm ${this.name}`);
},
};
person.greet(); // "Hi, I'm Alice"
// Lost 'this' context
const greetFn = person.greet;
// greetFn(); // "Hi, I'm undefined" (or error in strict mode)
// Fix with bind
const boundGreet = person.greet.bind(person);
boundGreet(); // "Hi, I'm Alice"
// call and apply
function introduce(greeting, punctuation) {
console.log(`${greeting}, I'm ${this.name}${punctuation}`);
}
introduce.call(person, "Hello", "!"); // "Hello, I'm Alice!"
introduce.apply(person, ["Hey", "."]); // "Hey, I'm Alice."
// Arrow function preserves 'this'
const team = {
name: "Engineering",
members: ["Alice", "Bob"],
printMembers() {
this.members.forEach((member) => {
console.log(`${member} is in ${this.name}`);
});
},
};
team.printMembers();
Prototype Chain
Every JavaScript object has an internal link to another object called its prototype, forming a chain that ends with null. When you access a property on an object, JavaScript first looks at the object itself, then walks up the prototype chain until it finds the property or reaches null. This mechanism is how JavaScript implements inheritance and shared behavior. Constructor functions and the 'new' keyword set up prototype relationships automatically, with each constructor's 'prototype' property becoming the prototype of instances it creates.
// Constructor function with prototype
function Animal(name, sound) {
this.name = name;
this.sound = sound;
}
Animal.prototype.speak = function () {
return `${this.name} says ${this.sound}`;
};
Animal.prototype.toString = function () {
return `[${this.name}]`;
};
const dog = new Animal("Rex", "woof");
const cat = new Animal("Whiskers", "meow");
console.log(dog.speak()); // "Rex says woof"
console.log(cat.speak()); // "Whiskers says meow"
// Checking the prototype chain
console.log(dog instanceof Animal); // true
console.log(Object.getPrototypeOf(dog) === Animal.prototype); // true
console.log(dog.hasOwnProperty("name")); // true
console.log(dog.hasOwnProperty("speak")); // false (on prototype)
// Prototype chain traversal
for (const key in dog) {
if (dog.hasOwnProperty(key)) {
console.log(`Own: ${key} = ${dog[key]}`);
} else {
console.log(`Inherited: ${key}`);
}
}
Object.create and Inheritance
Object.create creates a new object with the specified prototype object, providing a clean way to set up inheritance without constructors. It is the most direct way to establish prototype chains and is often preferred for its simplicity and explicitness. You can pass a property descriptor map as the second argument to define properties on the new object simultaneously. This pattern enables differential inheritance where objects inherit from other objects rather than from classes.
// Object.create for inheritance
const animal = {
init(name) {
this.name = name;
return this;
},
speak() {
return `${this.name} makes a noise.`;
},
};
const dog = Object.create(animal);
dog.bark = function () {
return `${this.name} barks!`;
};
const rex = Object.create(dog).init("Rex");
console.log(rex.speak()); // "Rex makes a noise."
console.log(rex.bark()); // "Rex barks!"
// Property descriptors
const config = Object.create(null, {
host: { value: "localhost", writable: false, enumerable: true },
port: { value: 3000, writable: true, enumerable: true },
});
console.log(config.host); // "localhost"
// config.host = "remote"; // Silently fails (or TypeError in strict)
// Object.assign for mixins
const serializable = {
toJSON() {
return JSON.stringify(this);
},
};
const loggable = {
log() {
console.log(`[${new Date().toISOString()}]`, this);
},
};
function createModel(data) {
return Object.assign(Object.create(null), serializable, loggable, data);
}
const model = createModel({ id: 1, name: "Test" });
console.log(model.toJSON());