Complete Guide to Master Intermediate JavaScript Skills in 2023

MikeyMikey
17 min read
  1. Learn Intermediate JavaScript

Welcome to our comprehensive guide on intermediate JavaScript concepts! If you're looking to take your JavaScript skills to the next level, you've come to the right place. In this article, we'll cover topics such as scope and closures, error handling, security, and more. We'll also provide exercises and interactive examples to help you solidify your understanding.

Before we begin, let's define our target audience. This guide is intended for individuals who have a basic understanding of JavaScript fundamentals, such as variables, data types, loops, and conditional statements. If you're not familiar with these concepts, we recommend checking out our beginner's guide to JavaScript first.

Basic of JS: https://draken.hashnode.dev/learn-javascript-fundamentals-for-beginners-in-2023

JavaScript has quickly become one of the most in-demand programming languages. While many start by learning the basics, taking your skills to the intermediate level and beyond is essential for tackling complex, real-world projects.

This comprehensive guide aims to provide everything you need to go from JavaScript fundamentals to intermediate abilities. Think of it as a roadmap to JavaScript mastery.

  1. Scope and Closures

One of the most important concepts in JavaScript is scope. Understanding scope is essential for writing clean, maintainable code. So, what is the scope exactly?

Scope refers to the region of the code where a variable is defined and can be accessed. In other words, it determines the visibility and lifetime of a variable. There are two main types of scope in JavaScript: local scope and global scope.

Local scope refers to the variables declared within a function. These variables are only accessible within the function itself and cannot be accessed outside of it. Here's an example:

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

greeter('Alice'); // Output: Hello, Alice!
console.log(name); // ReferenceError: name is not defined

As you can see, the variable name is only defined within the greeter function. Therefore, attempting to log it outside of the function results in a reference error.

On the other hand, global scope refers to variables declared outside of any function. These variables are accessible from anywhere in the script. Here's an example:

let name = 'Bob';

console.log(name); // Output: Bob

function greeter() {
  console.log(name); // Output: Bob
}

In this example, the variable name is declared in the global scope, which means it can be accessed from within any function.

Now, let's talk about closures. A closure is a function that has access to variables in its outer context. In other words, a closure "remembers" the values of all the variables that were in its scope when it was created. Here's an example:

function outer() {
  let name = 'Alice';

  function inner() {
    console.log(name); // Output: Alice
  }

  return inner;
}

const innerFunc = outer();
innerFunc(); // Output: Alice

In this example, the outer function defines a variable named name and returns an inner function that logs the value of the name. Even after the outer function has returned, the inner function still has access to the name variable because it was "closed over" when it was created.

Why is this useful? Well, closures allow us to create reusable functions that don't pollute the global scope. They also enable us to memoize expensive computations, which we'll discuss later in this guide.

  • Function scope - Variables defined in a function are accessible within it

  • Block scope - let and const are scoped to blocks like if statements

Block scope avoids unintended overwriting of values:

function calculate() {
  const x = 1; // function scoped 

  if (true) {
    const y = 2; // block scoped
  }

  console.log(x); // 1
  console.log(y); // ReferenceError  
}

Only x is accessible outside the block. Block scope catches many bugs!

function calculate() {

  // x is function scoped
  const x = 1;

  if (true) {
    // y is block scoped 
    const y = 2; 
  }

  console.log(x); // 1
  console.log(y); // ReferenceError
}

Flexible Functions

Functions allow reusable logic:

function calculateBill(total, taxRate) {
  return total * (1 + taxRate);
}

Arrow functions, default parameters, rest parameters, and spread syntax make them even more useful:

// Arrow function
const tipAmount = (total, tipPercent) => total * tipPercent;

// Rest parameters  
function sumAll(...nums) {
  return nums.reduce((sum, n) => sum + n); 
}

// Arrow function
const add = (a, b) => a + b; 

// Default parameters
function greet(name = "friend") {
  console.log("Hello " + name);
}

// Rest parameters
function sumAll(...numbers) {
  // sums numbers
}

// Spread syntax
const arr1 = [1, 2];
const arr2 = [...arr1, 3, 4];

Experiment with these features to master functions deeply.

  1. Object-Oriented JavaScript

While JS uses prototypal inheritance, the class syntax makes OOP easier:

class Person {
  constructor(name) { 
    this.name = name;
  }

  greeting() {
     console.log(`Hello ${this.name}!`)
  }
}

let john = new Person("John");
john.greeting(); // "Hello John!"
class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    console.log("Hello " + this.name);  
  }
}

const person = new Person("John");
person.greet(); // "Hello John"
  1. Prototypes and Inheritance

In JavaScript, objects inherit directly from other objects through their prototype property. This is known as prototypal inheritance.

// Animal class 
function Animal(name) {
  this.name = name; 
}

// Add animalSound() method
Animal.prototype.animalSound = function() { 
  console.log("Some sound");
}

// Inherit from Animal
function Cat(name) {
  Animal.call(this, name); // call super constructor
}

// Set prototype to Animal prototype
Cat.prototype = Object.create(Animal.prototype); 

Cat.prototype.animalSound = function() {
  console.log("Meow"); // override sound
}

let cat = new Cat("Snowy");
cat.animalSound(); // "Meow"

This allows objects to inherit methods and share behavior without classes.

This enables encapsulation, inheritance, and polymorphism.

These core concepts provide the foundation. Next, let's apply them to building apps.

  1. The DOM (Document Object Model)

The DOM allows accessing and manipulating HTML and CSS through JavaScript.

You can:

  • Select elements

  • Modify text, HTML attributes and styles

  • Create new nodes

  • React to events like clicks

// Select button
const button = document.querySelector('button');

// Listen for click event  
button.addEventListener('click', () => {
  // Show alert on click   
  alert('Button clicked!');  
});

DOM skills let you build interactive user interfaces.

  1. REST APIs

REST (REpresentational State Transfer) APIs allow client-server communication.

Common HTTP methods include:

  • GET - Retrieve data

  • POST - Create new data

  • PUT - Update existing data

  • DELETE - Delete data

// GET request  
fetch('/api/users')
  .then(res => res.json()) // parse JSON
  .then(users => {
    // Do something with user data returned
  })

// POST request
fetch('/api/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ name: 'Sara' })  
});

Consuming REST APIs enables powerful data-driven applications.

  1. Developing Real-World JavaScript Applications

With a solid grasp of language essentials, we can start constructing full-scale applications.

Real-World Project Examples:

  1. Simple Chat App: Build a basic chat app that allows users to send messages to each other. This project can demonstrate the use of JavaScript for client-side scripting, handling user input, and updating the interface in real-time.

  2. Weather App: Create a weather app that retrieves weather data from an API and displays the current temperature, humidity, and forecast for a given location. This project can showcase how to fetch and parse data from an external source, display dynamic content, and implement event listeners.

  3. Pomodoro Timer: Develop a simple Pomodoro timer that helps users manage their work intervals and break times. This project can illustrate the use of JavaScript for timing events, creating interfaces, and implementing logic flows.

  4. Todo List App: Build a todo list app that allows users to add, remove, and mark tasks as completed. This project can demonstrate the use of JavaScript for managing data, creating interfaces, and implementing functionality such as drag-and-drop interactions.

  5. Quiz App: Create a quiz app that tests users' knowledge of JavaScript concepts. This project can showcase how to create a responsive UI, validate user input, and implement gamification features such as scoring and leaderboards.

Making Websites Interactive

The DOM API allows controlling page content with JavaScript:

const btn = document.querySelector('button');

btn.addEventListener('click', () => {
  // Do something
});

This enables UIs that dynamically react to user input.

Consuming External APIs

APIs allow interaction with third-party services:

// GET data from API  
async function getUsers() {
  const resp = await fetch('/api/users');
  return resp.json();
}

// POST data to API
async function postData(url, data) {
  const resp = await fetch(url, {
    method: 'POST',
    body: JSON.stringify(data)
  });
  return resp.json();
}

Async/await provides clean asynchronous code.

  1. Using Modern JavaScript

Newer versions include many improvements:

Asynchronous Programming (Async/Await)

Async/await provides cleaner promise-based asynchronous code:

Async code runs non-blocking, essential for performance. JavaScript enables async through:

  • Callbacks - Functions passed as arguments to be executed later

  • Promises - Represent eventual completion of async tasks

  • Async/await - Clean syntax for promises

  • Event listeners - Trigger actions based on events like clicks

  • Timers - execute code after a time delay

  • Web workers - Run scripts on separate threads

async function fetchUsers() {
  const response = await fetch('/api/users');
  const users = await response.json();
  return users; 
}

const users = await fetchUsers();

Async functions always return promises enabling async composition:

async function processData() {
  const data = await fetchData();
  const filteredData = await filterData(data);
  return visualizeData(filteredData);
}

Optional Chaining

Optional chaining allows safe nested property access:

const user = {
  address: {
    street: '123 Main St'
  }
};

const street = user?.address?.street; // Won't throw if nullish

This avoids tedious null checking when accessing deeply nested values.

Nullish Coalescing

Nullish coalescing ?? provides a default value for null/undefined:

const userAge = user.age ?? 0;  
//0 if user.age is null/undefined

Much cleaner than verbose || defaulting.

Object Destructuring

Destructuring allows neatly extracting object properties:

const user = {
  id: 42,
  displayName: 'jdoe',
  fullName: {
    firstName: 'John', 
    lastName: 'Doe'
  }
};

// Extract fields from nested objects  
const { id, fullName: { firstName } } = user;

console.log(id); // 42   
console.log(firstName); // John

No more repetitive users. x to access deeply nested fields!

Many more modern features exist like private class fields, decorators, logical assignments, etc. Keep learning!

Exercise: Refactor some older JavaScript code using modern features like async/await, bullish coalescing, optional chaining, and object destructuring.

// Concise arrow functions
const sum = (x, y) => x + y;

// Object destructuring
const { name, age } = user; 

// Array methods  
const nums = [1, 2, 3];
const doubled = nums.map(x => x * 2);

// Classes
class Person {
  constructor(name) {
    this.name = name;  
  }
}

// Async/await  
async function init() {
  // Async code 
}
  1. Memoization

Memoization is a technique used to store the results of expensive computations so that they can be quickly retrieved the next time they're needed. This can significantly speed up the execution of our code, especially when dealing with complex algorithms or large datasets.

In JavaScript, we can use the memoize function to cache the results of a computation. Here's an example:

function fibonacci(n) {
  if (n <= 1) {
    return n;
  }

  const memoizedFibonacci = memoize(fibonacci, {
    maxSize: 50,
    threshold: 20
  });

  return memoizedFibonacci(n - 1) + memoizedFibonacci(n - 2);
}

console.log(fibonacci(7)); // Output: 13
console.log(fibonacci(8)); // Output: 21
console.log(fibonacci(9)); // Output: 34

In this example, the fibonacci function uses the memoize function to cache the results of previously computed values. This allows us to compute the Fibonacci sequence much faster than we would by computing it from scratch every time.

  1. Recursion

Recursion is a programming technique where a function calls itself repeatedly until it reaches a base case. It's a powerful tool for solving problems that have a recursive structure, such as tree traversals or factorial calculations.

However, recursion can also be slow and memory-intensive, especially when dealing with large inputs. To avoid these issues, we can use techniques like memoization or iteration to optimize our recursive functions.

Here's an example of a recursive function that calculates the factorial of a number:

function factorial(n) {
  if (n <= 1) {
    return 1;
  }

  return n * factorial(n - 1);
}

console.log(factorial(5)); // Output: 120

In this example, the factorial function calls itself repeatedly until it reaches the base case of n = 1. While this implementation works correctly, it can become very slow and memory-intensive for larger inputs.

We've covered critical skills for building robust apps. Let's look at tools next.

  1. Error Handling

Error handling is a critical aspect of JavaScript development. When writing code, we must anticipate potential errors and handle them gracefully. Otherwise, our users may encounter unexpected errors that ruin their experience.

Several types of errors can occur in JavaScript:

  1. Syntax errors: These are errors that occur due to incorrect syntax. For example, forgetting a semicolon at the end of a statement.

  2. Runtime errors: These are errors that occur while the code is running. For example, trying to access a property of null or undefined.

  3. Logic errors: These are errors that occur due to incorrect logic. For example, infinite loops or conditionals that never evaluate to true.

To handle errors in JavaScript, we can use try-catch blocks. The try block contains the code that might throw an error, while the catch block handles the error. Here's an example:

try {
  const userName = navigator.userAgent;
  console.log(UserName); // Output: Chrome
} catch (error) {
  console.error(error); // Output: TypeError: Cannot read property 'userAgent' of undefined
}

Here's another example of how we can use a try-catch block to handle errors:

// Make an API call
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => {
    // Process the data here
  })
  .catch(error => {
    // Handle the error here
  });

In this example, we're making an API call using the fetch() method. We're then processing the response data using the .json() method. But, if there's an error with the request or the response, we catch it using the .catch() method and handle it accordingly.

Best Practices for Error Handling

Now that we understand how errors work in JavaScript, let's talk about some best practices for handling errors in your code.

  1. Use specific error messages: When catching an error, make sure to include a specific error message that describes the issue. This will help developers who may be debugging your code understand what went wrong and how to fix it.

  2. Catch errors as close to the source as possible: It's generally a good idea to catch errors as close to the source as possible, rather than letting them bubble up to a higher level. This makes it easier to identify and fix issues quickly.

  3. Don't swallow errors: Avoid using empty catch blocks that simply log the error without providing any details. This can make it difficult to diagnose issues down the line. Instead, make sure to provide detailed information about the error, including its type and any relevant stack traces.

  4. Use a consistent error hierarchy: Consider creating a custom error class that extends the built-in Error class. This allows you to define a set of properties and behaviors that all errors in your application should follow. You can then create subclasses for different types of errors, such as authentication errors, validation errors, or network errors.

  5. Provide recovery options: In some cases, it may be possible to recover from an error by offering alternative solutions or fallback strategies. For example, if a required resource is not available, consider providing a default option or suggesting alternatives.

  6. Communicate with the user: Finally, remember to communicate clearly with the user when an error occurs. Provide helpful messaging that explains what went wrong and how they can resolve the issue. Be transparent about what steps your application is taking to address the problem, and offer ways for users to contact support if needed.

By following these best practices, you can write more robust and reliable code that handles errors effectively and communicates clearly with users.

  1. Mastering Build Tools and Frameworks

To work on large professional projects, you need deep knowledge of the JavaScript ecosystem.

Mastering the JavaScript Ecosystem

Becoming an intermediate JavaScript developer means going beyond the language itself. You need to master the broader ecosystem of tools and technologies.

Package Managers

npm and Yarn allow installing and managing reusable packages.

You can:

  • Install dependencies like React or lodash

  • Publish and share your own modules

  • Manage semantic versioning

  • Script common tasks with npm scripts

npm install react

npm run build

Dependencies and build tools streamline development.

Bundlers like Webpack

Bundlers combine your code and dependencies into optimized static assets.

Advantages include:

  • Import modules without global scope pollution

  • Bundle CSS, images, fonts, etc

  • Split code into chunks

  • Minify and optimize production builds

  • Hot reloading for rapid development

// Import lodash into src/index.js
import _ from 'lodash';

// Webpack bundles into dist/bundle.js

Bundling automates complex build workflows.

Linters and Formatters

Linters like ESLint analyze code for potential errors.

Formatters like Prettier enforce consistent code style.

// ESLint can catch common bugs   
const name = null;
name.length; // TypeError!

// Prettier auto-formats code
const wellFormatted = true;

Linting and formatting improves code quality and readability.

Exercise: Set up a simple Webpack build for a project. Learn how to process assets like CSS and images.

Testing JavaScript

Writing tests improves code quality and prevents bugs:

  • Unit testing - Isolate and test functions

  • Integration testing - Test component interconnections

  • UI testing - Simulate user interactions

  • Linting - Catch errors and enforce style rules

  • Prettier - Automatic code formatting

Exercise: Add Jest unit tests to a JavaScript function. Then run ESLint to fix linting errors.

These tools make you a more productive and professional developer.

Comprehensive testing is critical for serious development.

Frameworks and Libraries

JavaScript frameworks help manage app complexity:

  • React

    • Component-based UI development

    • Manages view layer only

    • Virtual DOM for fast rendering

    • React Hooks for stateful logic

    • Huge ecosystem of tools

  • Vue

    • Gradual adoption - use what you need

    • HTML-based templates

    • Reactivity system to manage state

    • Approachable for beginners

  • Angular

    • Full MVC framework

    • TypeScript-based

    • Complex enterprise applications

    • Steeper learning curve

Pick a framework and build some test projects to gain experience.

Exercise: Create a simple app that displays blog posts using React or Vue. Fetch data from a mock API endpoint.

The ecosystem provides indispensable tools for pro-level work. Next, let's look at improving holistically as a developer.

  1. Ten Tips for Building a JavaScript Project

Here is practical advice for building a full-scale JavaScript project:

  1. Plan Requirements and Architecture

    • Define scope, features, and target users

    • Architect flow between components and data models

  2. Use Version Control

    • Store code in Git/GitHub for history and collaboration
  3. Break Into Manageable Modules

    • Modular architecture eases development
  4. Document As You Go

    • Comment complex parts and create README docs
  5. Set Up Build System

    • Use Webpack, Rollup, Parcel etc to bundle modules
  6. Follow Style Guidelines

    • Consistent code with ESLint, Prettier, and guides
  7. Write Tests

    • Unit test critical pieces, integration test workflows
  8. Handle Errors Gracefully

    • Use try/catch blocks, handle network errors
  9. Optimize Performance

    • Profile slow parts, optimize bottlenecks
  10. Refactor Mercilessly

    • Refactor for readability, conciseness, and modularity

Planning, documenting, testing, optimizing, and refactoring are crucial skills that will help you build maintainable projects.

Exercise: Follow these tips to build a sample application, like a weather app or simple blog.

  1. Leveling Up Your Abilities as a Developer

Beyond learning technical concepts, advancing your career requires other well-rounded skills:

Keep Building!

Continuously build projects to develop practical skills:

  • Attempt coding challenges to learn new techniques

  • Clone interesting apps from GitHub

  • Experiment with unfamiliar tech

  • Contribute to open-source projects

  • Build and deploy your apps

Stay Curious!

Remaining continuously excited about learning is key:

  • Explore trending GitHub repos

  • Read release notes for new versions

  • Watch tech talks and conference videos

  • Don't stagnate on comfortable skills - keep exploring!

Get Involved!

Connecting with other developers accelerates your progress:

  • Attend Meetups and conferences

  • Join Discord channels and online communities

  • Write technical blog posts and tutorials

  • Participate in open-source

  • Follow top developers and learn from them

    1. Prepare for Interviews

Interviewing is a skill to work on like any other:

  • Practice explaining concepts clearly

  • Work through common algorithm questions

  • Review frequently asked JavaScript questions

  • Polish your portfolio with great projects

  • Practice common questions on concepts like:

    • Closures and scope

    • Prototypal inheritance

    • Async patterns

    • DOM manipulation

    • Class vs functional components (React)

    • Fetching data (React/Vue)

    • Time/space complexity

Interviewing is a skill in itself. Leetcode and Neetcode have great practice resources.

Leveling up requires effort in all aspects of coding - stay committed!

  1. Conclusion and Additional Resources

This guide provided a comprehensive roadmap for taking your JavaScript skills to the next level, from intermediate to advanced. Here are some parting tips:

  • Learn patiently, focus on regular practice

  • Continuously build projects to reinforce skills

  • Read docs and tutorials constantly

  • Participate in the JavaScript community

For more learning resources, I recommend:

Here's a quick recap of what we covered:

  • JavaScript fundamentals - syntax, variables, data structures, functions

  • Core concepts - scope, closure, OOP, asynchronous

  • Building apps - DOM, APIs, ES6+ features

  • Ecosystem - npm, Webpack, frameworks, testing

I hope you found this guide helpful for intermediate your JavaScript abilities.

0
Subscribe to my newsletter

Read articles from Mikey directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mikey
Mikey

Undergrad Student in domain of Web developer at Chandigarh University.