Hello! Welcome to the final lesson of our module on Core ES6+ Syntax. So far, we've explored individual features like arrow functions, destructuring, and enhanced object literals. Now it's time to put it all together.
Today's learning outcome is to refactor legacy JavaScript code to use modern ES6+ syntax and features. This is a practical skill that bridges the gap between knowing the syntax and applying it effectively. We'll take older, more verbose JavaScript (often called ES5) and transform it into the clean, readable, and powerful code expected in modern development, especially in frameworks like React.
As a Laravel developer, you're familiar with how modern PHP and frameworks provide elegant ways to handle common tasks. This lesson is about bringing that same modern sensibility to your JavaScript code.
Why Refactor?
Refactoring to ES6+ isn't just about using new syntax; it's about improving your code in several key ways:
- Readability: Modern syntax is often more concise and expressive.
- Maintainability: Cleaner code is easier to debug and extend.
- Fewer Bugs: Features like
let/constand lexicalthishelp prevent common sources of errors.
Let's dive into the most common refactoring patterns. We'll use a "before" and "after" approach for each.
1. From var to let and const
One of the most fundamental changes in modern JS is how we declare variables. The old var keyword had confusing scoping rules (function-scoped, not block-scoped) that could lead to subtle bugs, especially in loops.
Before (var):
The i variable "leaks" out of the for loop, and due to how closures work with setTimeout, all the delayed functions log the final value of i (which is 3).
// loop.js
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Prints "3", "3", "3"
}, 100);
}
// after loop
console.log(i); // Prints "3". The variable `i` is still accessible.
After (let):
By simply changing var to let, we fix both problems. let is block-scoped, so each loop iteration gets its own i variable. The variable is also not accessible outside the loop block.
// loop.js
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Prints "0", "1", "2"
}, 100);
}
// after loop
// console.log(i); // This would cause a ReferenceError: i is not defined
The general rule for refactoring is:
- Replace all
varwithlet. - If a variable's value is never reassigned, change
lettoconstfor immutability.
ES6 Tutorial: Learn Modern JavaScript in 1 Hour
For a clear demonstration of why var in loops is problematic and how let solves it, watch this segment from the Programming with Mosh tutorial.
Watch from 02:10 to 04:05. The video clearly explains the block-scoping behavior of let versus the function-scoping of var using a for loop example.
2. From function and .bind(this) to Arrow Functions
Arrow functions provide a more concise syntax, but their most significant advantage is how they handle the this keyword. They don't have their own this context; instead, they inherit it from their surrounding (lexical) scope. This solves a very common problem in older JavaScript.
Before (function):
Inside the forEach callback, this refers to the global object (or is undefined in strict mode), not our obj. The common workaround was to save the context in a variable, often called self or that.
const obj = {
name: "John",
friends: ["Jane", "Bob"],
printFriends: function() {
var self = this; // The workaround
this.friends.forEach(function(friend) {
// `this` here is not `obj`, so we use `self`
console.log(self.name + " knows " + friend);
});
}
};
obj.printFriends();
After (arrow function):
The arrow function inherits this from printFriends, which is correctly bound to obj. No more self workaround is needed.
const obj = {
name: "John",
friends: ["Jane", "Bob"],
printFriends() { // Also using method shorthand!
this.friends.forEach(friend => {
// `this` here is correctly inherited from the surrounding scope
console.log(`${this.name} knows ${friend}`); // Also using template literals!
});
}
};
obj.printFriends();
This pattern is incredibly useful in event listeners, setTimeout, and array methods—all places where callbacks are common.
3. From Prototypes to class
JavaScript's original object-oriented model was based on prototypes, which many developers found confusing and verbose. ES6 class syntax is "syntactic sugar" over prototypes, providing a much cleaner and more familiar structure, similar to what you've seen in PHP.
Before (Prototypes):
This involves a constructor function, adding methods to the prototype, and a complex setup for inheritance using Object.create and .call.
// Constructor function
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
return "Hello, I'm " + this.name;
};
// Inheritance
function Employee(name, age, title) {
Person.call(this, name, age); // Call parent constructor
this.title = title;
}
Employee.prototype = Object.create(Person.prototype); // Set up inheritance chain
Employee.prototype.constructor = Employee; // Fix constructor property
var emp = new Employee("John", 30, "Developer");
After (class):
The class, constructor, extends, and super keywords make this process vastly more intuitive.
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, I'm ${this.name}`;
}
}
class Employee extends Person {
constructor(name, age, title) {
super(name, age); // Call parent constructor
this.title = title;
}
}
const emp = new Employee("John", 30, "Developer");
ES6 Javascript Tutorial For Beginners | ES6 Crash Course
The video 'ES6 Javascript Tutorial For Beginners' provides a detailed walkthrough of refactoring from a prototype-based approach to ES6 classes.
Watch from 34:54 to 38:00 to see the setup of a constructor function and prototype. Then watch from 42:24 to 46:04 to see how the same logic is rewritten cleanly using ES6 class and extends.
4. From Script Tags/IIFEs to ES6 Modules
Before ES6, there was no native module system. Code was organized using multiple <script> tags, which polluted the global scope, or Immediately Invoked Function Expressions (IIFEs) to create private scopes. Modern JavaScript relies on ES6 modules (import/export).
Before (CommonJS - often seen in Node.js):
// math.js
function add(a, b) { return a + b; }
module.exports = { add: add };
// app.js
var math = require('./math.js');
console.log(math.add(2, 3));
After (ES6 Modules):
This is the standard in modern browsers and build tools. It allows for static analysis, tree-shaking (removing unused code), and clearer dependency management.
// math.js
export function add(a, b) { return a + b; }
export const PI = 3.14;
// app.js
import { add, PI } from './math.js';
console.log(add(2, 3));
We'll be using this system extensively when we start building React applications.
Refactoring Practice
Now, let's put everything into practice. Below is a block of legacy ES5 code. Your task is to refactor it using the modern ES6+ features we've discussed across this module.
Your Task: Refactor the following code snippet.
// Legacy Code to Refactor
var 'user-role' = 'admin'; // Using a string for a property key
function User(name, email) {
this.name = name;
this.email = email;
}
User.prototype.sayHello = function() {
return 'My name is ' + this.name;
};
User.prototype.displayProfile = function() {
var userContainer = document.getElementById('profile');
var profileData = {
name: this.name,
email: this.email,
role: 'user-role' // This is incorrect, it should use the variable above
};
// A timeout to simulate a delayed UI update
setTimeout(function() {
var message = 'User ' + profileData.name + ' with role ' + profileData.role + ' loaded.';
userContainer.textContent = message;
}.bind(this), 1000); // Using .bind() to fix `this` context
};
var user1 = new User('Alex', 'alex@example.com');
user1.displayProfile();
Things to look for:
vardeclarations.- String concatenation.
- The
functionkeyword for methods and callbacks. - The
.bind(this)workaround. - Prototype-based "classes".
- Object creation that could be more dynamic or concise.
Take your time and try to apply as many ES6 features as you can. Once you're done, compare your result with the solution below.
Click to see the refactored solution
// Refactored ES6+ Code
const roleProperty = 'user-role';
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
sayHello() {
return `My name is ${this.name}`;
}
displayProfile() {
const userContainer = document.getElementById('profile');
const profileData = {
name: this.name, // could use shorthand: `name`
email: this.email, // could use shorthand: `email`
[roleProperty]: 'admin' // Computed property name
};
// Use an arrow function to lexically bind `this`
setTimeout(() => {
// Use template literals and destructuring for cleaner access
const { name } = this;
const role = profileData[roleProperty];
const message = `User ${name} with role '${role}' loaded.`;
userContainer.textContent = message;
}, 1000);
}
}
const user1 = new User('Alex', 'alex@example.com');
user1.displayProfile();
For a comprehensive guide covering all these transformations with interactive examples, I highly recommend bookmarking this cheatsheet. It's an excellent resource for quick reference.
Interactive JavaScript ES6, ES2016, and ES2017 cheatsheet!
The 'Interactive JavaScript ES6, ES2016, and ES2017 cheatsheet' is a fantastic resource that shows 'old way' vs 'new way' code side-by-side for all the features we've covered. Use it to solidify your understanding and for future reference.
Explore the sections on 'Variable Declarations', 'String Templates', 'Destructuring', 'Spread Operator', 'Arrow Functions', 'Classes', and 'Modules'. The interactive code blocks are especially useful for experimentation.
Conclusion
Congratulations on completing this module! You've now covered the essential syntax and patterns that form the foundation of modern JavaScript. Refactoring is a skill that sharpens with practice, and you'll find these patterns become second nature as you build more projects.
Key Takeaways:
- Variables: Prefer
constby default, useletonly for variables that need to be reassigned. Avoidvar. - Functions: Use arrow functions for concise syntax and to avoid
this-related issues in callbacks. - Objects & Classes: Use
class,extends, andsuperfor cleaner OOP, and leverage enhanced object literals for concise object creation. - Strings & Arrays: Use template literals, destructuring, and the spread operator to write more declarative and readable code.
- Organization: Use
importandexportto create modular, maintainable codebases.
Preview of Next Lesson:
With a solid grasp of synchronous JavaScript, we're ready to tackle one of the most crucial topics for web development: asynchronous operations. In the next lesson, you'll explain the roles of the call stack, event loop, and callback queue, which are the fundamental building blocks for understanding how JavaScript handles tasks like API requests and timers without freezing the browser. This will set the stage for mastering Promises and async/await.