As a seasoned JavaScript developer with 5+ years of experience, you're likely no stranger to the intricacies of JavaScript. However, even the most experienced developers can benefit from a refresher on the most critical concepts and nuances of the language. In this article, we'll cover the top 20 JavaScript interview questions that can help you prepare effectively for your next interview.
Anonymous functions provide a concise way to define functions, especially useful for simple operations or callbacks. They are commonly used in:
map()
, filter()
, and reduce()
.Checkout the below code example-
// Encapsulating Code using IIFE(function () {// Some code here.})();// CallbackssetTimeout(function () {console.log('Hello world!');}, 1000);// Functional programming constructsconst arr = [1, 2, 3];const double = arr.map(function (el) {return el * 2;});console.log(double); // [2, 4, 6]
A closure is a function that retains access to these variables even after the outer function has finished executing. This is like the function has a memory of its original environment.
function outerFunction() {const outerVar = 'I am outside of innerFunction';function innerFunction() {console.log(outerVar); // `innerFunction` can still access `outerVar`.}return innerFunction;}const inner = outerFunction(); // `inner` now holds a reference to `innerFunction`.inner(); // "I am outside of innerFunction"// Even though `outerFunction` has completed execution, `inner` still has access to variables defined inside `outerFunction`.
Closures are useful for:
Pros:
Avoid callback hell: Promises simplify nested callbacks.
// Callback hellgetData1((data) => {getData2(data, (data) => {getData3(data, (result) => {console.log(result);});});
Sequential code: Easier to write and read using .then()
.
Parallel code: Simplifies managing multiple promises with Promise.all()
.
Promise.all([getData1(), getData2(), getData3()]).then((results) => {console.log(results);}).catch((error) => {console.error('Error:', error);});
Cons:
AbortController
in JavaScript?AbortController
allows you to cancel ongoing asynchronous operations like fetch requests. To use it:
const controller = new AbortController();
2.Pass the signal: Add the signal to the fetch request options.controller.abort()
to cancel the request.Here is an example of how to use **AbortController
**s with the fetch()
API:
const controller = new AbortController();const signal = controller.signal;fetch('YOUR API', { signal }).then((response) => {// Handle response}).catch((error) => {if (error.name === 'AbortError') {console.log('Request aborted');} else {console.error('Error:', error);}});// Call abort() to abort the requestcontroller.abort();
Some of its use cases can be:
fetch()
request on a user actionIn JavaScript it's very easy to extend a built-in/native object. You can simply extend a built-in object by adding properties and functions to its prototype
.
String.prototype.reverseString = function () {return this.split('').reverse().join('');};console.log('hello world'.reverseString()); // Outputs 'dlrow olleh'// Instead of extending the built-in object, write a pure utility function to do it.function reverseString(str) {return str.split('').reverse().join('');}console.log(reverseString('hello world')); // Outputs 'dlrow olleh'
While this may seem like a good idea at first, it is dangerous in practice. Imagine your code uses a few libraries that both extend the Array.prototype
by adding the same contains
method, the implementations will overwrite each other and your code will have unpredictable behavior if these two methods do not work the same way.
Extending built-in objects can lead to issues such as:
In the browser, the global scope refers to the top-level context where variables, functions, and objects are accessible throughout the code. This scope is represented by the window object. Variables and functions declared outside of any function or block (excluding modules) are added to the window object, making them globally accessible.
For example:
// This runs in the global scope, not within a module.let globalVar = 'Hello, world!';function greet() {console.log('Greetings from the global scope!');}console.log(window.globalVar); // 'Hello, world!'window.greet(); // 'Greetings from the global scope!'
In this example, globalVar and greet are attached to the window object and can be accessed from anywhere in the global scope.
Generally, it's advisable to avoid polluting the global namespace unless necessary. Key reasons include:
In JavaScript, modules are reusable pieces of code that encapsulate functionality, making it easier to manage, maintain, and structure your applications. Modules allow you to break down your code into smaller, manageable parts, each with its own scope.
CommonJS is an older module system that was initially designed for server-side JavaScript development with Node.js. It uses the require()
function to load modules and the module.exports
or exports
object to define the exports of a module.
// my-module.jsconst value = 42;module.exports = { value };// main.jsconst myModule = require('./my-module.js');console.log(myModule.value); // 42
ES Modules (ECMAScript Modules) are the standardized module system introduced in ES6 (ECMAScript 2015). They use the import
and export
statements to handle module dependencies.
// my-module.jsexport const value = 42;// main.jsimport { value } from './my-module.js';console.log(value); // 42
Immutability is a core principle in functional programming but it has lots to offer to object-oriented programs as well.
Mutable objects in JavaScript allow for modifications to their properties and values after creation. This behavior is default for most objects.
let mutableObject = {name: 'John',age: 30,};// Modify the objectmutableObject.name = 'Jane';console.log(mutableObject); // Output: { name: 'Jane', age: 30 }
Mutable objects like mutableObject
above can have their properties changed directly, making them flexible for dynamic updates.
In contrast, immutable objects cannot be modified once created. Any attempt to change their content results in the creation of a new object with the updated values.
const immutableObject = Object.freeze({name: 'John',age: 30,});// Attempting to modify the objectimmutableObject.name = 'Jane'; // This change won't affect the objectconsole.log(immutableObject); // Output: { name: 'John', age: 30 }
Here, immutableObject
remains unchanged after creation due to Object.freeze()
, which prevents modifications to its properties.
The primary difference lies in modifiability. Mutable objects allow changes to their properties directly, while immutable objects ensure the integrity of their initial state by disallowing direct modifications.
const
vs immutable objectsA common confusion is that declaring a variable using const
makes the value immutable, which is not true at all.
Using const
prevents the reassignment of variables but doesn't make non-primitive values immutable.
// Using constconst person = { name: 'John' };person = { name: 'Jane' }; // Error: Assignment to constant variableperson.name = 'Jane'; // Allowed, person.name is now 'Jane'// Using Object.freeze() to create an immutable objectconst frozenPerson = Object.freeze({ name: 'John' });frozenPerson.name = 'Jane'; // Fails silently (no error, but no change)frozenPerson = { name: 'Jane' }; // Error: Assignment to constant variable
In the first example with const
, reassigning a new object to person
is not allowed, but modifying the name
property is permitted. In the second example, Object.freeze()
makes the frozenPerson
object immutable, preventing any changes to its properties.
Static class members in JavaScript, denoted by the static
keyword, are accessed directly on the class itself, not on instances. They serve multiple purposes:
class Config {static API_KEY = 'your-api-key';static FEATURE_FLAG = true;}console.log(Config.API_KEY); // Output: 'your-api-key'console.log(Config.FEATURE_FLAG); // Output: true
class Arithmetic {static add(a, b) {return a + b;}static subtract(a, b) {return a - b;}}console.log(Arithmetic.add(2, 3)); // Output: 5console.log(Arithmetic.subtract(5, 2)); // Output: 3
class Singleton {static instance;static getInstance() {if (!this.instance) {this.instance = new Singleton();}return this.instance;}}const singleton1 = Singleton.getInstance();const singleton2 = Singleton.getInstance();console.log(singleton1 === singleton2); // Output: true
Symbol
s used for in JavaScript?Symbols in JavaScript, introduced in ES6, are unique and immutable identifiers primarily used as object property keys to avoid name collisions. They can be created using the Symbol()
function, ensuring each Symbol value is unique even if descriptions are identical. Symbol properties are non-enumerable, making them suitable for private object state.
const sym1 = Symbol();const sym2 = Symbol('uniqueKey');console.log(typeof sym1); // "symbol"console.log(sym1 === sym2); // false, each symbol is uniqueconst obj = {};const sym = Symbol('uniqueKey');obj[sym] = 'value';console.log(obj[sym]); // "value"
Key characteristics include:
for...in
loops or Object.keys()
.Global Symbols can be created using Symbol.for('key')
, allowing reuse across different parts of codebases:
const globalSym1 = Symbol.for('globalKey');const globalSym2 = Symbol.for('globalKey');console.log(globalSym1 === globalSym2); // trueconst key = Symbol.keyFor(globalSym1);console.log(key); // "globalKey"
There are some well known Symbol
in JavaScript like:
Symbol.iterator
: Defines the default iterator
for an object.Symbol.toStringTag
: Used to create a string description for an object.Symbol.hasInstance
: Used to determine if an object is an instance of a constructor.JavaScript object getters and setters are essential for controlling access to object properties, offering customization when getting or setting values.
const user = {_firstName: 'John',_lastName: 'Doe',get fullName() {return `${this._firstName} ${this._lastName}`;},set fullName(value) {const parts = value.split(' ');this._firstName = parts[0];this._lastName = parts[1];},};console.log(user.fullName); // Output: 'John Doe'user.fullName = 'Jane Smith';console.log(user.fullName); // Output: 'Jane Smith'
Getters (fullName
) compute values based on internal properties (_firstName
and _lastName
), while setters (fullName
) update these properties based on assigned values ('Jane Smith'
). These mechanisms enhance data encapsulation and allow for custom data handling in JavaScript objects.
Tools and techniques for debugging JavaScript code vary depending on the context:
debugger
statement: Inserting debugger;
in code triggers breakpoints when Devtools are open, pausing execution for inspection.console.log()
debugging: Using console.log()
statements to output variable values and debug messages.Currying in JavaScript is a functional programming technique where a function with multiple arguments is transformed into a sequence of nested functions, each taking a single argument. This allows for partial application of the function's arguments, meaning you can fix some arguments ahead of time and then apply the remaining arguments later.
Here's a simple example of a curry function and why this syntax offers an advantage:
// Example of a curry functionfunction curry(fn) {return function curried(...args) {if (args.length >= fn.length) {return fn(...args);} else {return function (...moreArgs) {return curried(...args, ...moreArgs);};}};}// Example function to be curriedfunction multiply(a, b, c) {return a * b * c;}// Currying the multiply functionconst curriedMultiply = curry(multiply);// Applying curried functionsconst step1 = curriedMultiply(2); // partially apply 2const step2 = step1(3); // partially apply 3const result = step2(4); // apply the final argumentconsole.log(result); // Output: 24
Advantages of Curry Syntax:
Currying enhances the functional programming paradigm in JavaScript by enabling concise, composable, and reusable functions, promoting cleaner and more modular code.
load
event and the document DOMContentLoaded
event?The DOMContentLoaded
event is triggered once the initial HTML document has been fully loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading.
In contrast, the window's load
event is fired only after the DOM and all dependent resources, such as stylesheets, images, and subframes, have completely loaded.
JSONP (JSON with Padding) is a technique used to circumvent cross-domain restrictions in web browsers, as standard Ajax requests to different domains are generally blocked.
Instead of using Ajax, JSONP makes a request to a cross-origin domain by dynamically creating a <script>
tag with a callback query parameter, such as: https://example.com?callback=handleResponse
. The server wraps the data in a function named handleResponse
and returns it.
<script>function handleResponse(data) {console.log(`User: ${data.username}`);}</script><script src="<https://example.com?callback=handleResponse>"></script>
For this to work, the client must define the handleResponse
function in the global scope, which will be invoked when the response is received.
JSONP poses security risks because it executes JavaScript from external sources. Therefore, it's crucial to trust the JSONP provider.
Nowadays, CORS is the preferred method, making JSONP largely obsolete.
The same-origin policy restricts JavaScript from making requests to different domains. An origin is specified by the combination of the URI scheme, hostname, and port number. This policy is crucial for security, as it prevents a malicious script on one page from accessing sensitive data on another page's Document Object Model (DOM). This ensures that data remains secure within its designated origin, blocking unauthorized cross-origin interactions.
Single Page Apps (SPAs) are highly interactive web applications that load a single HTML page and dynamically update content as the user interacts with the app. Unlike traditional server-side rendering, SPAs use client-side rendering, fetching new data via AJAX without full-page refreshes. This approach makes the app more responsive and reduces the number of HTTP requests.
Pros:
Cons:
To enhance SEO for SPAs, consider:
In the past, developers often used Backbone for models, promoting an OOP approach by creating Backbone models and attaching methods to them.
While the module pattern remains useful, modern development often favors React/Redux, which employs a single-directional data flow based on the Flux architecture. Here, app data models are typically represented using plain objects, with utility pure functions to manipulate these objects. State changes are handled using actions and reducers, following Redux principles.
Avoid classical inheritance when possible. If you must use it, adhere to best practices and guidelines.
Possess working knowledge of it. Promises can be fulfilled, rejected, or pending, and allow users to attach callbacks for handling outcomes.
Common polyfills include $.deferred
, Q, and Bluebird. However, with ES2015 providing native support, polyfills are typically unnecessary.
Attributes: Defined in HTML tags, they provide initial info for the browser (like "Hello" in <input type="text" value="Hello">
).
Properties: Belong to the DOM (JavaScript's view of the page), allowing you to access and change element info after the page loads (like updating the text field value).
You made it till the end! I hope you found these JavaScript questions helpful in preparing for your next interview. As an experienced developer, mastering both foundational and advanced concepts is key to showcasing your expertise and acing your next interview. But the most important thing to remember is that it's okay to not know everything - it's all about being willing to learn and improve.