Complete Guide to Master Intermediate JavaScript Skills in 2023
Fundamental Concepts for Intermediate JavaScript
Table of contents
- Learn Intermediate JavaScript
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.
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.
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"
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.
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.
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.
Developing Real-World JavaScript Applications
With a solid grasp of language essentials, we can start constructing full-scale applications.
Real-World Project Examples:
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.
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.
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.
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.
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.
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
}
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.
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.
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:
Syntax errors: These are errors that occur due to incorrect syntax. For example, forgetting a semicolon at the end of a statement.
Runtime errors: These are errors that occur while the code is running. For example, trying to access a property of null or undefined.
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.
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.
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.
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.
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.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.
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.
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.
Ten Tips for Building a JavaScript Project
Here is practical advice for building a full-scale JavaScript project:
Plan Requirements and Architecture
Define scope, features, and target users
Architect flow between components and data models
Use Version Control
- Store code in Git/GitHub for history and collaboration
Break Into Manageable Modules
- Modular architecture eases development
Document As You Go
- Comment complex parts and create README docs
Set Up Build System
- Use Webpack, Rollup, Parcel etc to bundle modules
Follow Style Guidelines
- Consistent code with ESLint, Prettier, and guides
Write Tests
- Unit test critical pieces, integration test workflows
Handle Errors Gracefully
- Use try/catch blocks, handle network errors
Optimize Performance
- Profile slow parts, optimize bottlenecks
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.
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
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!
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:
MDN JavaScript Docs - Excellent reference resource
Eloquent JavaScript - The best JS book available
JavaScript30 - 30 projects for hands-on learning
Frontend Masters - In-depth video courses
r/javascript - JavaScript discussion forum
CodeNewbie - Helpful for beginners
JavaScript Tutorial: https://www.w3schools.com/js/
JavaScript Guide: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide
JavaScript Book: https://eloquentjavascript.net/
JavaScript30: https://javascript30.com/
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.