JavaScript Interview Questions Coding Interview Question | Skilled.dev
Interview Question

JavaScript Interview Questions

Course launching August 2020

Follow me on YouTube for free coding interview videos.

Users who sign up for the email list will receive an exclusive 75% discount at launch.

Your subscription could not be saved. Please try again.
Your subscription was successful! 🤓
This lesson requires a subscription >> Upgrade

This article contains the most commonly asked JavaScript question, and they are all rooted in the fundamentals. If you understand these, you will have the intuition and JavaScript foundation to perform well in interviews.

There are more advanced JavaScript interview questions as well in this module:

The goal in this article is two things:

  1. Test on essential JavaScript knowledge and core fundamentals that can come up in interviews
  2. Give examples of common questions (some of these are "good" and some are not but still are asked frequently, so you need to know them)

Questions

AJAX stands for "asynchronous JavaScript and XML" and is a core feature of the modern web. It allows us to create fast, dyamic, and interactive web applications.

In the client/browser, instead of needing to transition pages, reload the current page, or submit a form to change the state, we can make HTTP requests asynchronously to send and receive data with a server in the background. This allows us to decouple the presentation layer from the data layer to create powerful web applications. We can update parts of the page by exchanging smaller amounts of data with the server without needing rebuild the entire page.

When we talk about making an API request on the client, it is doing so through AJAX. JSON has replaced XML as the primary data format and is the current standard. When AJAX was first introduced, the XMLHttpRequest pattern was used to facilitate AJAX requests, but now the more moden method is to use fetch or some similar promise-based library.

In JavaScript, functions are first-class citizens which means they can be treated like any other data type. A function can be assigned to a variable, passed as argument to another function, or be the return value from another function.

An anonymous functions means we use a function without declaring or naming it. The most common example of this in JavaScript is a callback function where we pass an anonymous function as an argument to be executed inside another function.

// Array.prototype.map: arr.map(callback)
const arr = [1, 2, 3];
const squaredArray = arr.map((item) => item * item); // [1, 4, 9]
// (item) => item * item is an anonymous function callback
// It is executed on each item

// setTimeout(callback, milliseconds)
setTimeout(function() {
  alert('I am alerting in a callback')
}, 1000);
// After 1000ms, the setTimeout function executes the anonymouns function callback

If a variable is declared in the global scope, it is available to the entire code which can cause collisions and difficult bugs. JavaScript is a function-scoped language which means that variables declared in a function will only exist in that function. Anonymous functions declared as a IIFE's would allow us to run code without poluting the global scope.

(function() {
  // a is only accessible in this scope
  var a = 1;
})();

Arrow functions offer a few advantages:

  1. A more concise and compact syntax. () => {} vs function() {}.

  2. Implicit return if the function is one line. You can leave our the function body and JavaScript will automatically return the value.

const double = (num) => 2 * num;
// Equivalent to
const double = function(num) {
  return 2 * num;
}
  1. The main benefit is how arrow functions handlethis. Previously, using function bound the value of this based on where the function was called. This forced developers to add hacky fixes to retain the original this value. On the other hand, an arrow function does not create its own this and instead uses the value from its enclosing scope.

Other notable differences include: arrow functions don't have acces to the arguments object, arrow functions don't change this from bind/call/apply since they don't define their own value for it, and arrow functions can't be used as constructors functions with the new keyword.

All three of these methods can be used to set the this value of a function. There are some small differences in how they world though.

This is still a common interview question and pattern you'll see in code. However, with the updates from ES2015 and movement toward functional programming, it is less common to use these methods in practice.

bind

This is used to set the this of a function, but does not invoke the function immediately. Instead it returns a new function with the content of it bound.

function sayHello() {
  console.log(`Hello, ${this.name}!`);
}

const bob = { name: 'Bob' };

// It creates a new function with its `this` set
const sayHelloToBob = sayHello.bind(bob);
sayHelloToBob(); // Hello, Bob!

call

The call method will execute a function immediately, and the first parameter is the object that is bound as this and all the arguments after it will be passed to the function.

function greetFriend(firstName, lastName) {
  console.log(`Hello, ${this.name}! Nice to meet you ${firstName} ${lastName}`);
}

const bob = { name: 'Bob' };

// It executes the function with `this` and passes params
greetFriend.call(bob, 'Dan', 'McCool');

apply

The apply method will execute a function immediately, and the first parameter is the object that is bound as this and the second argument is an array where the items will each be individually passed to the function.

function greetFriend(firstName, lastName) {
  console.log(`Hello, ${this.name}! Nice to meet you ${firstName} ${lastName}`);
}

const bob = { name: 'Bob' };
const names = ['Dan', 'McCool'];

// It executes the function with `this` and passes params
greetFriend.apply(bob, names);

call vs apply

Both of the functions execute immediately. The primary different is that call will take an arbitrary number of arguments where all past the first will be parameters to the executed function. The apply method only takes two arguments where the second argument is an array that pass its items as arguments. Sometimes these methods are simply used to execute a function, and if the this content is not necessary, you can use null as the first argument.

When laying our a web page, all the elements are treated as rectangles. CSS determines the position, size, and style of a box.

A box comprised of margin, border, padding, and content.

  • margin: The outermost area of a box. It enforces the distance (empty space) a box wants to have from its surrounded neighbors
  • border: The border is the outer edge of the element and sits inside the margin.
  • padding: The empty space inside of an element between the edge (border) and the content of the element.
  • content: This contains the real content of an element/box which is the text, image, video, etc.

The box-sizing property is used to indicate how we want to determine the size of a given box. Web development primary uses box-sizing: border-box; where the border and padding are included in an elements width and height.

Closures can be confusing for newer developers, but once the concept "clicks", they become very intuitive. The two features of JavaScript that enable closures are:

  1. Lexical scoping (variables take on values based on where they were declared)
  2. Functions as first-class citizens (specifically that a function can be returned as a value from another function)

The key to forming a closure is returning a function from another function. This returned function now has access to all the variables of the outer function because it is part of its scope. The values of the variables persist and can be read/updated.

const outerFunction = () => {
  let storedValue = 0;

  // Returning a function from another function is the key to closures
  return () => {
    storedValue = storedValue + 1;
    console.log('storedValue = ', storedValue);
  }
}

// Assign the returned function to a new variable
const newFunctionWithPermanentAccess = outerFunction();

// Execute the returned funciton to change the value it has access to (via a closure)
newFunctionWithPermanentAccess(); // storedValue = 1;
newFunctionWithPermanentAccess(); // storedValue = 2;
newFunctionWithPermanentAccess(); // storedValue = 3;

CORS stands for "cross-origin resource sharing" and is a mechanism for how we are able to access resources from other domains (origins). It specifically refers to how a client connects to servers. CORS determines how a browser is able to utilize files/data from domains outside a user's current URL. Because a website loads resources from all around the web, the same-origin is used to mitigate risk and prevent hijacking a user's visit.

For example, if you are on facebook.com, you are able to download all the HTML, CSS, JavaScript, font, and image files from them. In addition, the browser won't block you from accessing an API like facebook.com/api/users. However, if you try to access data from a domain outside of Facebook while still on the site, such as calling the Twitter API twitter.com/api, you may or may not be able to fetch this data depending on how CORS is configured.

Because web applications have become so complex and dynamic, an understanding of CORS is essential and typicaly dictates the functionality of our clients. Running into a case where CORS is not enabled is a common issue that needs to be handled.

CORS is determined by the servers that provide the files/data. They use the different Access-Control-* headers to dictate how their data can be accessed in browsers.

The same-origin policy only applies to the client. If there is a resource you need where CORS isn't enabled, you can access it using your server and then respond from your server to the client.

Debugging is an inevitable part of coding, and there are claims that we spend anywhere from 20-90% of our time debugging. Because of this, companies want to understand that you can do it effectively, especially if there is a bug that is in production affecting real users.

Debugging can appear in interviews in a few ways:

  1. Discuss your process of debugging
  2. Tell me about a time you debugged a difficult issue
  3. Be given a program program and asked to debug and/or refactor it

Often this isn't one of those questions that has a "right" answer, but it's typically more of a discussion to understand your thought process and experience.

Possible points to mention:

  1. Using console.log. There isn't anything wrong using this, although some people like to frown upon it. If it is your preferred method, just be prepared to justify why.
  2. Breakpoints and stepping through them in the browser.
  3. Investigating files in the browser
  4. Looking at network requests and assess the request and response
  5. Debugging Node code with breakpoints through your text editor
  6. Using source maps for transpiled code
  7. Testing and how you use it to reduce bugs
  8. Static type checking such as TypeScript
  9. Using a tool like Sentry to capture bugs
  10. Investigating server logs for causes
  11. Deployment and rollback process for produciton apps
  12. Determining out how to reproduce a bug from production

This questions looks to see if you understand fundamentals of the internet and how DNS works.

A URL is just a human-readable string to remember a website. Behind the scenes, clients/servers actually connect via an IP address, and there are a series of steps taken to translate a domain name to an IP.

There are series of steps that are followed until the IP address is discovered:

  1. The user enters a URL.
  2. The client checks to see if they have the IP address cached locally that matches this URL.
  3. Then a resolving nameserver is queried for the IP address. This is often your internet service provider (ISP). If it does not have the IP cached, it will then go through the process to find it for you.
  4. The resolving server queries the root server which responds with the address of the Top Level Domain (TLD) server (such as .com or .dev), which stores information for all of its domains.
  5. The TLD server is queried and responds with the nameserver that stores the IP address of the domain we want (the domain's nameserver).
  6. The domain's nameserver (called the authoritative nameserver) is queried for the IP address of the domain we're requesting.
  7. The IP address is returned to the resolving nameserver which passes it to the client and can communicate with the server through the IP address matching the domain.

DOM stands for Document Object Model. A web page is just a text document written in HTML, and the DOM is the data representation of this HTML document using objects to describe its stucture and content. The DOM treats the HTML document as a tree data structure where each node is an object that represents a part of the document.

The DOM can be represented using any programming language (at its core, it's just a data structure), but we most commonly think of it in JavaScript since that is the language of the browser. The DOM allows programmatic access to its tree by exposing methods that allow us to update the structure, style, and co

  • The DOM is the standard with which we represent a web page as a data structure
  • The DOM is a tree data structure
  • The nodes in the tree are JavaScript objects
  • The objects expose methods and data about a particular section of the web page and allow us to interact with and update it.

JavaScript is a loosely typed language which means we don't have to declare a variable's type at creation, and a variable can change types throughout the lifetime of the program. Because of this loose typing, JavaScript introduced two ways to compare variables with === and ==.

To a computer, something like 2 and "2" are entirely different, and in JavaScript the first is considered a number type and the second is a string type. Using the === operator, we are trying to determine two items are "strictly equal" (they must match in type and value). If we use ==, JavaScript will try to coerce the values (convert them to the same type) and then compare them. The intention here is that we only care if the values are similar, even if they are originally of the same type which is often called "loosely equal".

The === comparison is almost always favored over == because it is more explicit and prevents unintended bugs. If you need to make comparisons in your interview code, I would highly recommend to never use === or be prepared to make a strong argument for why you did.

The following are some of the weird cases when JS tries to convert values and compare them:

0 == false; // true
0 == ''; // true
0 == '0'; // true
1 == '1'; // true
1 == [1]; // true
1 == true; // true
null == undefined; // true

These would all return false with ===.

Event bubbling is part of the process where a browser captures user interaction, such as a click event or a keyboard event.

The DOM is a nested data structure where the window is the root, the document is its child, and every node on the web page are children of it. So when you click an element on the page, you have also clicked all of its parent elements as well. The browser then determines which element you intended to click.

There are three phases of a browser event:

  1. Capture: The event starts at the window and traverse through the DOM until it reaches the most deeply nested element that you clicked.
  2. Target: The event reaches the target element.
  3. Bubbing: The event bubbles up from this target and returns to the parent to denote the completion and handling of the event.

The bubbling phase is when we actually execute the callbacks based on an event, so this is most commonly the one we care about.

Bugs can arrise from event bubbling when we have an event handler on a parent of the intended target node. The target will execute its callback, and the parent will as well unless we stop the event from bubbling up to it.

The browser may also have some default intended behavior when we a certain event is triggered on an element, and we may wish to prevent that as well.

To block default behavior or stop an event from behind handled by parent nodes, we have the functions:

  • preventDefault(): Prevent the browser from executing its default behavior from an event. For example, clicking a checkbox and executing this function will stop it from checking and unchecking.
  • stopPropagation(): Stop the propagation an event from continuting in either the capture or bubbling phase.
  • stopImmediatePropagation(): Stop the propagation an event from continuting in either the capture or bubbling phase AND stop any further events that are being handled on the current element.
Credit: javascript.info

Event delegation is when you attach an event listener to a parent instead of its children.

For example, imagine you had a list <ul> with 10000 children <li> nodes. If you were to attach an onclick handler to each <li>, it could require the browser to use significant resources to create, maintain, and remove all of these.

On the other hand, you could attach one onclick handler to the <ul> element. Then if you were to click an <li>, you could recognize and handle this event during the bubbling phase.

The event loop (CONCEPT MODAL? LINK TO ARTICLE?) the foundation of how JavaScript executes code. You may or may not be asked this directly, but the concepts will be helpful regardless. Understanding it will not only help for interviews, but make you a better JS programmer overall.

The two terms you would want to mention are the "call stack" and the "event queue".

  • Call Stack: Where our main program runs synconously
  • Event Queue: Where the callbacks of asyncronous code run once the stack is clear
  • Event Loop: Watches the stack, and when it's empty, it grabs the first item in the event queue and passes it to the stack to run
  • Heap: Where we store our objects and variables in memory

You may have heard that JavaScript is "single-threaded non-blocking IO". When we say JS is single-threaded, it means the main program runs in-order on a single thread by adding items to the stack. However, when we say it's non-blocking IO, this means tasks that aren't part of our main program are actually pushed off to another thread to handle concurrently, and then the result is passed to a callback and added to the event queue to handle once the stack is clear.

If you haven't watched this video by Philip Roberts, it's essential content for any JS developer.

The this keyword in JavaScript is one of the most challenging topics in JavaScript. Even if a developer knows the theory behind it, it can still be challenging to work with in real code. Explaining this fully would require a book.

In interviews, what you will most often be tested on is:

  • Giving a simple explanation of this
  • Fix code where this isn't working as expected
  • Build an object that uses this inside it

The most difficult part with this is that its value is determine by where the function is call that uses it. So even if you define to work a certain way, this can still change at any point in your program. There are six rules to help you determine what the value of this will be.

  1. When you create an object using the new keyword with a constructor function/class, this will refer to the new object inside the function.
  2. Using bind, call, or apply will override the value inside a function, and you can hardcode its value for this.
  3. If a function is called on an object as a method, this will refer to the object that is calling it. For example, object.method() would have a value of this that refers to object.
  4. If a function is executed without any of the three previous criteria being applied, this will refer to the global object - window in the browser or global in Node. If you are using strict mode, this will be undefined instead of the global object.
  5. If multiple rules from above apply, it will use the rule that comes first.
  6. Arrow functions ignore all the above rules, and the value of this is determined by the scope enclosing the arrow function.

A question like this is more of meant to generate than looking for a specific answer. They want to hear what frameworks you have used and the opinions you have about it (pros and cons). They also want to see if you understand modern web development and how applications are built in the real-world.

A very important note - don't mention a framework unless you are prepared to discuss it and compare it to other frameworks. An interviewer isn't looking to see if you have tried every single technology. They are try to see if you understand modern web development and justify the design decisions that you have made. You can briefly mention something like "I tried Angular for a simple project, but really focused on React afterward because I like how React...". But if you only know React (or any other single framework), really hone in on what benefits you think it provides.

For example, let's consider React.

Pros

  • Component architecture
  • Flexibility
  • Focused on being as close to JavaScript as possible (minimal special syntax)
  • Make complex applications feel simpler and easy to manage
  • Single responsibility principle. It only focuses on the view / presentation layer, and the developer chooses how to handle other aspects of the app (ie. data).
  • Strong community
  • Applications are fast (although many gained ground and event surpassed)
  • Engaged core development team
  • Constantly evolving the library but with a focus on backward compatibility
  • Cross-platform with React Native

Cons

  • Challenging handling of this
  • SSR can be difficult
  • Unopinionated. Because of React's flexibility, it can also hurt productivity because teams are required to pick their own patterns and libraries, whereas other opinionated frameworks enforce you build an app a certain way
  • Reliant on the community for solutions since they aren't all baked into the core library
  • Backed by a big tech company (some developers don't want to feel bolted to the whims of an entity like Facebook)
  • It was one of the first compnent libraries, so it has some old patterns that newer libraries have improved

These are three ways that functions appear in our code.

  • function Car(){} is how we declare a function
  • const car = Car() executes a function at sets its return value (which can be anything) to car
  • const car = new Car() creates an instanct of a Car object and assigns this object to car

LINK TO A CONCEPT? LINK TO A YDKJS BOOK?

Both the browser (window) and node (global) have a global outer scope which is managed by a global object. Variables and functions declared in the global scope are available anywhere in your code. Standard library functions we use in our program are actually stored on the global object:

setTimout(() => { console.log('hi') }, 1000);
// Is the same as...

// ... in the browser
window.setTimout(() => {
  window.console.log('hi');
}, 1000);

// ... in Node
global.setTimout(() => {
  global.console.log('hi');
}, 1000);

If you declare a variable or function at the top-level, it actually becomes a property on the global object.

Declaring a variable with let or const does not add it to the global object.

var a = 'hi';

function logger() {/*...*/}

a === window.a; // true
logger === window.logger; // true

We should be careful when adding variables to the global object because it can cause collisions and unexpected bugs that are hard to track down. For example, a common issue is two modules that are expecting a different value stored in the same global variable name.

JavaScript treats functions as first class citizens which allows for some very powerful patterns. A higher-order function is one that takes a function as a parameter and/or its return value is a function.

Common examples this include:

  • A callback function to handle the result of an asyncronous call
  • A callback to dictate how to execute, such as arr.map(x => x * x)
  • Function currying where the returned value is another function that can be executed
// Curry the add function
const add = a => b => a + b;
const add5 = add(5);

add5(10) // 15
add5(37) // 42
add5(0) // 5

// Execut both sequentially
add(1)(2) // 3

Variables declared using the var keyword or functions declared using the function keyword will have their declaractions moved to the top of their scope.

// Hoisting
console.log(hello) // undefined
var hello = 'world';
console.log(hello) // world

// This is equivalent to
var hello;
console.log(hello) // undefined
hello = 'world';
console.log(hello) // world

Immutability means that if we want to update a value, we create a new variable that contains the new value instead of changing a previous variable. Immutability is a core pattern in functional programming, and it gives us more confidence in our programs because we know data cannot change throughout the lifetime.

JavaScript doesn't offer immutable arrays or objects, but there are functions we can use to prevent objects from being altered and patterns we can follow to prevent data from being mutated. Strings in JavaScript are immutable.

Some examples of maintaining immutability in JS are:

Note that Object.assign and the spread operator just make a shallow copy of the object. If you have nested arrays/objects, they will still point to the original value.

// Spread operator in arrays
const arr1 = [1, 2, 3];
const arr2 = [...arr1];
// This creates a brand new array with the same values
arr1 === arr2; // false

// Spread operator in objects
const obj1 = { hi: 'world' };
const obj2 = { ...obj1 };
// This creates a brand new object with the same properties
obj1 === obj2;// false

// Object.assign
const obj1 = { hi: 'world' };
const obj2 = Object.assign({}, obj1);
// This creates a brand new object with the same properties
obj1 === obj2; // false

// Array.slice
const arr1 = [1, 2, 3];
const arr2 = arr1.slice(1);
console.log(arr1); // [1, 2, 3]
console.log(arr2) ;// [2, 3]

JavaScript also offers lower-level configuration of objects.

You can use Object.defineProperty to set the CRUD access on individual properties. Setting writable: false means that you can't change the value of a property, and setting configurable: false means you can't change the type or delete it from the object.

const obj = {};

Object.defineProperty(obj, 'hello', {
  value: 'world',
  writable: false,
  configurable: false,
});

console.log(obj.hello); // 'world'
obj.hello = 'Skilled.dev';
console.log(obj.hello); // 'world'

You can use Object.preventExtensions to block an object from adding new properties.

var obj = {
  hello: 'world',
};

Object.preventExtensions(obj);

obj.test = 123;
console.log(obj.test); // undefined

// You are still allowed to change existing properties
obj.hello = 'Skilled.dev';
console.log(obj) // { hello: 'world' }

You can use Object.seal which is the same as Object.preventExtensions, but it also sets all th properties to configurable: false. You cannot add new properties, change the type of existing properties, or delete properties.

var obj = {
  hello: 'world',
};

Object.seal(obj);

delete obj.hello;

console.log(obj); // { hello: 'world' }

Using Object.freeze gives you the highlest level of immutability. You cannot add, change, or delete properties.

var obj = {
  hello: 'world',
};

Object.freeze(obj);

obj.test = 123;
obj.hello = 'Skilled.dev';
delete obj.hello;

console.log(obj); // { hello: 'world' }

Immutable data has been growing in popularity with JavaScript developers. It means that once a variable or object is declared, it can't be altered or updated. This gives us increased stability and condifence in the state of our application.

If an object is mutable, this means that it can be modified after it is created.

Advantages of Immutability

  • Simplified programs without fear of objects evolving or changing through a program's lifetime. If we pass an object around to different functions or different threads, we know it won't change at any point.
  • Change detection. In JavaScript, objects are store in variables as pointers. If we create a new object, it takes on a new memory address and thus a new pointer. If we alter an existing object, we will have the same pointer and would have to inspect every property to determine if there was an update.
  • Better memory management. We can cache an object in memory and use this single copy as much as needed because all functions know they're accessing a read-only copy of the same data.
  • Optimize CPU, memory, and rendering. If we only need to maintain a single copy of any object, we can share it everywhere prevent any recalculations based on the data which will only be triggered if a new object is created.

Downside of Immutability

  • It's easy to mess up immutability in JS. If you're relying on immutability but someone implements it incorrectly, it can cause challenging bugs. One common example of this is using a spread ... which only makes a shallow copy, and nested objects may still be altered.
  • If you use immutable objects that are frequently destroyed and recreated for updates, it can decrease performance as opposed to just altering a property.

A key takeaway is that immutability can either help or hurt performance. If you want to enforce it strictly, it's usually best use a library that is optimized and also enforces immutability well.

The Math.max method takes an arbitrarily long list of parameters and returns the largest.

Math.max(2, 4, 10, 20, 100); // 100

We instead want it to work with an array.

const nums = [2, 4, 10, 20, 100];
Math.max(nums); // NaN

The above returns NaN, so we'll write our own function arrayMax that still uses Math.max to find the largest number in an array.

The simplest solution is the spread operator .... It will decompose an array into its individual items.

function arrayMax(arr) {
  return Math.max(...arr);
}
arrayMax([2, 4, 10, 20, 100]); // 100

This question has historically been used to test an understanding of apply. It will pass an array as individual items to a function. Math.max is also a static method which means it is called with instantiating an object and does not require access to this, so we pass null as the first argument.

function arrayMax(arr) {
  return Math.max.apply(null, arr);
}
arrayMax([2, 4, 10, 20, 100]); // 100

These three values may seem similar but actually have very different impacts on our code.

  • null: declared and explicitly assined an empty value
  • undefined: declared and not assigned a value
  • undeclared: trying to use a variable that was never declared

null

If a variable is set to null, we are explicitely saying this value is empty.

Properties in JSON objects can be set to null, and it will persist through an HTTP request. If another programming language uses the same JSON object, they will have their own null construct to handle the value.

In JavaScript, if we have default function parameters and pass a null input, it will use the null and not the default.

const explicitlyEmpty = null;

// Serializes to JSON
JSON.stringify({ explicitlyEmpty }); // "{ "explicitly": null }"

// Does not trigger default parameters
const sayHello = (name = 'world!') => {
  console.log(`Hello, ${name}`);
}
sayHello(explicitlyEmpty);

// Checking for null
explicitlyEmpty === null; // true

undefined

An undefined variable means it has been declared but not assigned a value (as opposed to null which has expicitely assigned the value).

If there is an undefined value in object that is converted to JSON, it will remove this property.

When using default function parameters, if there is no value passed, it is viewed as undefined and will use the default value.

Developers often use typeof x === 'undefined' to check for undefined because it won't throw an error if the variable is actually undeclared.

let empty;

// Does NOT serialize to JSON
JSON.stringify({ empty }); // "{}"

// Does not trigger default parameters
const sayHello = (name = 'world!') => {
  console.log(`Hello, ${name}`);
}
sayHello(empty);

// Checking for null
empty === undefined; // true
typeof empty === 'undefined'; // true

undeclared

Undeclared is the terminology we use when describing we try to use a variable in our code that was never declared at all.

// We will get a ReferenceError because x is not declared
x + 1; // ReferenceError

// It still throws a reference error if we try to do this
x === undefined; // ReferenceError

// This works with undeclared variables
typeof x === 'undefined'; // true

Since ES2015, new features have been added the JavaScript specification. Unfortunately not all browsers implement the new features, or our users are still using old browsers. To allow developers to use the new features and be compatible with all browsers, we use polyfills and/or transpile our code.

A polyfill is a piece of code that implements new JavaScript features in ES5 JavaScript compatible with browsers. For example, we can use a polyfill for the Promise object to use promises in any browser.

Another option to use new features and syntax is to transpile code. Webpack or another bundler takes our modern code and transforms it to a version all browsers can read. For example, if you tried to use spread operator ..., it would throw an error in an old browser.

This is a deep topic that takes a book to understand fully. I'll give a synopsis of how to understand it for an interview, but I highly recommend "YDKJS: this & Object Prototypes" for a complete understanding.

To understand prototypes, it's probably best to first explain class inheritance vs. protoype inheritance.

NOTE: The JavaScript class keyword is not an actual class like in other languages. It is still just a wrapper around prototypes but uses syntax that will feel similar to other languages.

Classes

A class is how most other object-oriented languages (ie. Python, Java, C++, etc) share functionality. Classes are a blueprint on how to create an object, and to inherit its functionality in a child class you copy all of the methods and properties over. They are duplicated in the child.

Classes themselves are not objects, but define the properties and methods an object will have.

Prorotypes

In JavaScript, we don't use classes as a blueprint. We use object themselves to share properties and methods. This means we do not make copies, and instead we link to an object through the prototype chain to indicate we want to utilize a parent's functionality.

Every object in JavaScript has a __proto__ property which points to the object it inherits from. The object it points to also has its own __proto__ which points to its parent. This is how we create the prototype chain - each object points to the object it inherits from until we reach Object that sits at the top of the prototype chain.

If we try to call a method or property on an object we create, but it doesn't exist on our object directly, JavaScript will then walk through the objects in prototype chain to see if they have it.

Benefits of Prototypes

One of the primary benefits of using prototypes over classes is that we don't duplicate properties on children, and this drastically reduces the memory footprint. We just have a single object for each level of inheritance that is referred when we need functionality. For example, when you create a React class component class MyComponent extends React.Component, each one just points to the Component object instead of copying over all the methods for every single component you make in your app.

Another benefit is that there is less coupling. We can compose objects to group functionality instead of bolting it to class. This approach can actually be much simpler and also more powerful once you understand prototypes.

Prototypes are also easily extended. For example, when a new version of JavaScript is released, we can easily modify and polyfill new functions on existing objects.

Prototype Example

The easiest way to use prototype inheritance and set an object's __proto__ is through the Object.create function. It creates a new object with the __proto__ set to the value passed to create.

const person = {
  getName() {
    return this.name;
  },
  getAge() {
    return this.age;
  }
}

// This sets bob.__proto__ = person;
const bob = Object.create(person);
// This sets dan.__proto__ = person;
const dan = Object.create(person);

console.log(bob.__proto__); // { getName, getAge }
console.log(dan.__proto__); // { getName, getAge }

// They have the same __proto__ (not a copy)
console.log(bob.__proto__ === person); // true
console.log(dan.__proto__ === bob.__proto__); // true

bob.name = 'Bob';
bob.age = 42;
console.log(`My name is ${bob.getName()} and I am ${bob.getAge()}`);

// If we change person, the objects that inherit from it will receive the properties/methods
person.TEST = '123';

console.log(bob.TEST);

new and prototype

A final note is that prototype is actually a property of functions that are used as constructors. All objects created using the new keyword with this constructor function, their __proto__ will point to the prototype of the constructor function.

function Person(name, age) {
  // this is automatically return when Person is used as a constructor
  this.name = name;
  this.age = age;
}

// `prototype` is a property of functions
// and will be passed to objects created using `new`
Person.prototype.getName = function() { return this.name };
Person.prototype.getAge = function() { return this.age };

console.log(Person.prototype); // { constructor, getName, getAge }

const bob = new Person('Bob', 42);
console.log(`My name is ${bob.getName()} and I am ${bob.getAge()}`);

console.log(bob.__proto__ === Person.prototype); // true

Person.prototype.TEST = 123;

console.log(bob.TEST); // 123
  • Client-side rendering: CSR means we render our applications entirely on the browser. When a page is first loaded, an empty HTML document is passed to the client with a <script> tag that retrieves JavaScript. Once this JavaScript loads, it builds the HTML for the page. CSR can be great for fast connections, but can produce a white stagnant screen if a connection is slow. CSR has been criticized for being bad for SEO because it requires the web crawler to be able to effectively run the JavaScript to build the page.
  • Server-side rendering: When the user requests a URL, the server generates the HTML for the first page load. All the JS and CSS files are passed down as well and once on the client, the additional pages behave as a single-page application. This is better for slow connections because the initial work is done on the server and provides an initial page without needing to download and execute JavaScript files. This also helps with SEO because the web crawler gets a full page at the intial request.
  • Static site geneeration: Pages of a website are built as static HTML files before deploying. This is the fastest method because the files are pre-built and can be cached in a CDN and there is no wait time for a server to render them, which means they can be sent to the client very quickly. Once the initial HTML is loaded, it is passed the JS and CSS files and often functions as a single-page application after this.

Short circuit evaluation is a technique where you use binary logical operators to avoid unnecessary work. It can also be used to assign variables based on the truthy or falsy values used.

A truthy value means that the value is coerced to true if its original type is not a boolean, and a value is falsy if its value is coerced to false (for example, an empty string '') if its original value is not a boolean.

Binary logical operators will look something like the following:

item1 || item2
item1 && item2
item1 ?? item2

The OR || operator will return item1 if it is truthy. If not, it will return item2. We use the || to short circuit by being able to skip the second item if the first is true.

true || true         returns true
true || false        returns true
false || true        returns true
false || false       returns false

// Short circuit with ||
// Will only do the expensive request if the user doesn't exist in cache
const user = cache.user || getUserFromExpensiveRequest();

The AND && operator will return item1 if it is falsy. It not, it will return item2. We use the && to short circuit by being able to skip the second item if the first is false. This is also a common pattern for conditional rendering in web applications.

true && true         returns true
true && false        returns false
false && true        returns false
false && false       returns false

// Short circuit with &&
// Will only perform the expensive calculation if you are logged in
isLoggedIn && doExpensiveCalculationIfLoggedIn();

// Conditional rendering
const canSeeComponent = true;

<div>
  {canSeeComponent && <RenderThisConditionally />}
</div>

The nullish coalescing operator ?? released in ES2020. It is similar to || but instead of considering all falsy values, it only yields to item2 if item1 is either null or undefined.

null ?? true         returns true
undefined ?? true    returns true
false ?? true        returns false

Before the modern internet, websites were multi-page, and each page would load independently. When you clicked a link, you would navigate to this new page and the server would send its HTML you would need to load the JS and CSS needed for it.

Single-page applications interact with the browser to dynamically rewrite the HTML on the page instead of reloading the page and receiving the HTML from a server. It instead requests only the data needed to populate a new URL.

The name "single-page" means that the page is only loaded once then it receives all the JavaScript and CSS code which allows it to build UI entirely on the client. This can offer big performance improvements because it only needs data to load new pages instead of loading an entirely new HTML document. It will also have all the necessary JS and CSS and won't need to load those each time (or will only need to download small incremental chunks to render a new URL).

Not only does it improve performance on the client, but it also reduces the load on the server. The backend becomes more focused on simply being an API and passing data as JSON to the frontend.

Before you begin optimizing your site, the first thing you need to do is diagnose the issues. This likely requires you getting into the browser developer console and checking network requests (time they time, payload sizes, headers used) and looking at performance charts of your code.

Apps are slow either because they take too much time to get the files/data they need to run, or the code itself has bad performance. To optimize your site, the solutions typically fall under three categories:

  1. Reduce the amount of bytes sent from a server
  2. Download the right resources at the right time
  3. Reduce expensive / long operations

Ways to speed up your application include:

  1. Compress your static files and minify code: Reduce the amount of data that needs to be sent.
  2. Cache static files correctly in the browser: This will speed up subsequent trips to the site. You must use the appropriate headers (etags and cache control) so the browser knows how long to store files.
  3. Use a CDN to cache static files: This has a few benefits: 1) It allows you to put files closer to your users 2) It allows you to spread out network requests (most modern browsers only allow six concurent requests to a single domain) 3) Users may have a required file already cached from a CDN 4) Reduces load on your server
  4. Optimize images: Make sure they are an appropriate size for how they are used on the page. Images are still just static assets, so we must make sure they cache correctly, use compression, and are served from a CDN.
  5. Load only the content necessary for a page and prioritize above the fold: You can chunk your JavaScript so it only loads what a pages needs. For content on the page, you can lazy load it and show it only when necessary. For example, only download images when they come into view.
  6. Minimize redirects: Sending a user through multiple URLs will ultimately slow down down the time it takes for them to get to the page.
  7. Progressive rendering: Instead of waiting for the entire data of the page to load, treat it as separate parts and display each piece as their data is received. This can require the server to expose better API endpoints or the client to call them correctly.
  8. Reduce API payload size: The less data that is sent, the faster the page can load.
  9. User SSR or SSG: Using server rendering or static page generation can allow you get content that a user can interact with without needing the entire JS bundle to download first.
  10. Minimize time to first bite: Fix any network issues and use dns-prefetch.
  11. Minimize slow operations: This can be either on the server or the client. For slow server calculations or database queries, you can cache the result in memory storage like Redis.
  12. Handle browser events effectively: Poor event handling can grind an app to a halt. For example, if you have an onscroll listener, use a throttle to minimize the number of callbacks that are executed.

Strict mode was added in ES5 as an optional opt-in feature. It is enabled by adding the string 'use strict;' to the top of a file or function. Strict mode is meant to make our code safer and more robust. ES2015 modules (code using the import/export syntax) are automatically in strict mode.

The overall goal is to make JavaScript more secure, efficient, and prepared for future updates.

The advantages of strict mode are:

  • Eliminates some silent JS errors by throwing them instead
  • Removes the ability to assign a variable without being declared (previously assigning a variable without var just became a global variable)
  • Makes JS more secure by how it handles this
  • Function parameter names must be unique
  • Simplifies the the usage of eval and arguments which allows for optimizations
  • It removes the ability to use features that are generally regarded as "bad"
  • Paves the way for new JavaScript features by disallowing future keywords such as private and interface

A JavaScript program executes exactly in-order on a single main thread. This means that code will run line-by-line synchronously. When our program encounters a synchronous function, it will add it to the call stack and the functions at the top of the call stack will run first. If a function takes a long time to finish, the one after must wait for it to complete. This means long synchronous functions can cause our app to pause and appear frozen if not handled correctly.

JavaScript also has the concept of asynchronous functions. This is code that is handled off the main thread and then the result is passed to a callback which can handle it after it completes. The callback is passed to the event queue, and once the call stack is empty, the event loop will begin passing items from the event queue to the call stack to run the callback that handles the async function response. Asynchronous functions prevent the main thread from being blocked and allow us to perform expensive operations in a separate thread or wait for the result of a side effect and continue execution of the main program.

Examples of asynchronous functions include an API calls, a database query, file system I/O, or waiting for a setTimeout.

A ternary operator means there are three operands. It is similar to an if-else statement: condition ? if : else. One difference to note is that inside an if-else statement, you execute code, but but with a ternary operator it returns either the first or second value and can be assigned to a variable.

Since JavaScript is a loosely typed language, we can use variables of any type and JavaScript will try to coerce them (transform their type) for whatever is needed in a particular situation. For example, and if (conditional) {...} statement is expecting a boolean, but you can pass any variable type to it, and it will still work.

const str = 'hello';
const emptyStr = '';

if (str) { console.log('this shows up'); }
if (emptyStr) { console.log('this will not log'); }

What is happening is that JavaScript is converting the string above to a boolean, and a string with characters is converted to true and an empty string is converted to false. Then the if statement handles the resulting coercion.

Every variable type is converted in this manner. The values that convert to true are considered "truthy". The values that convert to false are considered "falsy".

Below is a list of all the falsy values in JavaScript. Everything else is considered truthy.

false
null
undefined
"" // empty string
NaN
0 // also -0 and 0n

It's worth noting that empty arrays [] and empty objects {} are considered truthy.

The var variable declaration is typically viewed as a legacy pattern. It creates a variable that is function scoped and can be reassigned at any time. If a var keyword is declared outside of a function, it is part of the global scope and will be a property on the global object.

// var can be reassigned
var hello = 'world';
hello = 'Skilled.dev';
console.log(hello) // Skilled.dev

function test() {
  var funcScoped = 123;
  console.log(funcScoped); // 123
}
console.log(funcScoped); // ReferenceError

if (true) {
  var worksOutsideBlocks = ':)';
  console.log(blockScoped); // :)
}
console.log(worksOutsideBlocks) // :)

The let keyword was introduced in ES2015 and has similarities to var. A variable declared with let can reassigned at any time.

The primary difference though is that let is block scoped which means it retains it lifetime within only a {} (including functions).

// var can be reassigned
let hello = 'world';
hello = 'Skilled.dev';
console.log(hello) // Skilled.dev

function test() {
  let funcScoped = 123;
  console.log(funcScoped); // 123
}
console.log(funcScoped); // ReferenceError

if (true) {
  let blockScoped = ':)';
  console.log(blockScoped); // :)
}
console.log(blockScoped) // ReferenceError

The const keywork is used for constant variables. An error will be thrown if we try to reassign a variable created using const. Like let, a const variable is block scoped.

// var can be reassigned
const hello = 'world';
hello = 'Skilled.dev'; // TypeError: Assignment to constant variable.

function test() {
  const funcScoped = 123;
  console.log(funcScoped); // 123
}
console.log(funcScoped); // ReferenceError

if (true) {
  const blockScoped = ':)';
  console.log(blockScoped); // :)
}
console.log(blockScoped) // ReferenceError
Prev
Essential JavaScript Interview Concepts
Next
Fix `this`
This lesson requires a subscription >> Upgrade

Table of Contents