This is part 3 of the “Ace Your Next JavaScript Interview” series where I cover the fundamentals of JavaScript, sharing my notes which I use to review and prepare for interviews.
In today’s article, I’ll cover other JavaScript concepts, such as this, new, Prototypes, and Classes.
If you missed the previous articles, you can check part 1 and part 2:
The this Keyword
In object-oriented languages like C# or Java, the this keyword refers to the current object of a method or a constructor.
So each time you use this inside a method of a object, it will always refer to the same object.
However, in JavaScript, the this keyword implementation is slightly different, which sometimes causes many problems and bugs, if not understood well.
Let explore some examples.
Example 1: Method Call
const hero = {
name: 'Aria',
greet() {
console.log(`${this.name} says hi`);
}
};
hero.greet(); // Aria says hi
In this case, everything works as expected.
However, the value of this is not determined by the function that uses it.
Example 2: Detached Method
const { greet } = hero;
greet(); // says hi
In this case, we get an empty value for this.name because the value of this depends on the call-site.
It doesn’t refer to the function in which it’s used or to it’s scope.
It refers to the object on which the function is being executed.
In the example above, I’ve run the code inside the browser, where this refers to the window object and it doesn’t have a name property.
💡 Pro tip: Always ask: “What object sits on the left side of the dot when the function is being called?”
We can say that **this** is not an author-time binding but a runtime binding.
It is contextual based on the conditions of the function’s invocation.
Apart from the mentioned example from above, there’re some other ways of how to manipulate the value of this.
Let’s explore them as well just for completeness.
Example 3: Arrow Functions
Arrow functions don’t get their own this.
Instead, they capture it from the surrounding scope.
const knight = {
name: 'Lancelot',
salute: () => console.log(this.name)
};
knight.salute(); // undefined
Example 4: Explicit Binding
We can explicitly set the value of this when we call a method.
This is called binding because we bind a specific value to this.
function introduce() {
console.log(`I am ${this.name}`);
}
const elf = { name: 'Legolas' };
introduce.call(elf); // I am Legolas
As you see, in JavaScript, we can use this inside functions, not only classes.
The new Keyword
In object-oriented languages like C# or Java, the new keyword is used to create a new instance of class.
However, in JavaScript, the behavior of the new keyword is slightly different, no matter there’re classes in the language.
In OO languages, using the new keyword will result in calling the constructor method of the class.
In JavaScript, we can simply use a function to create a new object.
function Hero(name) {
this.name = name;
}
const hero = new Hero("Gandalf");
console.log(hero.name); // "Gandalf"
When we use the new keyword before a function, it will create a new object and bind the this inside the function to the newly created object.
Then, it will return the new object, if the function doesn’t return anything.
If the function returns an object, the above is not valid.
function Person(name) {
this.name = name;
return { nickname: `The ${this.name}` };
}
const p = new Person("T-Shaped Dev");
console.log(p.nickname); // "The T-Shaped Dev"
console.log(p.name); // undefined
Note: Arrow functions can’t be used as constructors. They throw if you use new.
Note On Using this And new
If you don’t want the headache of using this and new, you might prefer a more functional approach.
You can use closures to deal with this and factory functions to deal with new.
For instance:
// Functional factory + Closure approach
function createCounter(start = 0) {
let count = start;
function increment() {
count += 1;
console.log(count);
}
function reset() {
count = 0;
console.log(count);
}
return { increment, reset };
}
const c2 = createCounter(5);
c2.increment(); // 6
c2.reset(); // 0
Prototypes
Objects in JavaScript have a prototype property which references another object.
So in JavaScript, we can build an inheritance object hierarchy through using Prototypes.
The other way of building such hierarchies is through Classes which are a syntax sugar over Prototypes.
Whenever you read a property of an object, it will first try to find it in the object itself.
If not found inside the object, it will look for it into the prototype.
If it’s not found there as well, it will check the prototype’s prototype and so on until it reaches the Object.prototype.
const hero = {
salute: () => console.log('Salute!')
};
const knight = {
name: 'Knight',
__proto__: hero
};
knight.salute();
In this case, we set the __proto__ property explicitly for the specific object.
To set up the inheritance chain, we can do it in the constructor function so every created object has the correct prototype.
function Hero(name) {
this.name = name;
this.salute = function () {
console.log(`${this.name}, Salute!`);
}
}
function Knight(name) {
this.name = name;
}
// create a reference for the prototype
Knight.prototype = new Hero();
const knight = new Knight("Lancelot");
knight.salute(); // Lancelot, Salute!
By setting the prototype object to the constructor function, we ensure that all objects created through the new keyword will set up properly.
Note: There are some other quirks around Prototypes in JavaScript, but I’ve found the shared notes above to be fundamental.
Classes
ES6 introduced Classes which are a syntax sugar over Prototypes.
Nowadays, using Prototypes is discouraged because of the high complexity around setting up big inheritance hierarchies.
It’s most common to use Classes also because of the familiarity of people with other OO languages.
class Hero {
constructor(name) {
this.name = name;
this.salute = function () {
console.log(`${this.name}, Salute!`);
};
}
}
class Knight extends Hero {
constructor(name) {
super(name)
}
}
const knight = new Knight("Lancelot");
knight.salute(); // Lancelot, Salute!
Composition
Inheritance is a powerful tool to extend entities and create more complex object but it’s not ideal for all use cases.
Creating complex inheritance hierarchies is not trivial and most of the time we might end up with unnecessary complexity.
Personally, I think that the best way to create complex objects in JavaScript is through composition.
This way, we create objects with only what they need, they combine them to create a more complex ones.
function canAttack(state) {
return {
attack: () => console.log(`${state.name} slashes!`)
};
}
function canHeal(state) {
return {
heal: () => console.log(`${state.name} heals!`)
};
}
function createPaladin(name) {
const state = { name };
return {
...canAttack(state),
...canHeal(state),
name
};
}
const paladin = createPaladin('Theron');
paladin.attack(); // Theron slashes!
paladin.heal(); // Theron heals!
Performance Considerations About Inheritance
It’s important to mention that if performance is critical to the app then defining a function to the prototype might be a better option, instead of creating a fresh new copy every time.
This means that the function will be created once and will leave on the shared prototype.
function FastFighter(name) {
this.name = name;
// per-instance method
this.attack = () => console.log(`${this.name} strikes fast!`);
}
// vs.
FastFighter.prototype.attack = function() {
console.log(`${this.name} strikes fast!`);
};
This can be helpful in some scenarios but might lead to problems if not used properly because we deal with references, instead of actual copies.
📌 TL;DR
thisis set by the call-site, not where a function lives.newcreates a fresh new object, bindsthis, and returns it.- Prototypes forms a chain, so objects can inherit missing properties.
- Classes are a syntax sugar over Prototypes.
- Prefer composition to mix small behaviors, instead of deep and large hierarchies.
- We can share code through using prototype methods, boosting performance.
