Skip to content
Journey to Code

JavaScript Evolution: From Scripting Language to Modern Development Platform

The Transformation of Modern JavaScript

What ES6 Changed

· By · Updated · 7 min read

Cover reading "The Transformation of Modern JavaScript" with an ES6 promise chain and an edition rail marking ES2015

A variable declared inside an if block is still visible after the block ends. A string with three values in it needs a pile of + signs. Code that waits for something needs a callback inside a callback.

In 2015 the language fixed all of those with ECMAScript 2015, better known as ES6. In this article, we are going to walk through the upgrade one habit at a time. We will show the old way break, then show the fix.

Quick recap if you are new here: before 2015, JavaScript (ES5) had one kind of variable declaration, var, which is visible across a whole function, and it handled waiting-for-something code with callbacks, meaning functions you hand over to be called later. Everything below assumes only that.

Historical Context: The Road to ES6#

The six-year gap between ES5 (2009) and ES6 (2015) represented a period of intense debate and innovation. During this time, the TC39 committee (the group that decides what goes into the language) worked diligently to develop new standards that would address JavaScript's growing pains as it evolved from simple websites to complex applications.

Languages like CoffeeScript showed developers the possibilities of what JavaScript could become. Transpilers, especially Babel (a transpiler rewrites newer code into older code that old browsers understand), played a big part in early adoption, allowing developers to write next-generation code while still supporting older browsers.

Core Language Enhancements#

Variables and Scoping: Bringing Order to Chaos#

Before ES6, JavaScript developers struggled with the quirks of var and function-scoped variables, leading to unexpected behaviors and hard-to-find bugs:

// Pre-ES6 problems with 'var'
for (var i = 0; i < 5; i++) {
  // 'i' is accessible outside this block!
}
console.log(i); // 5 - often an unintended consequence

ES6 introduced block-scoped let and const, giving developers precise control over variable lifetimes:

// ES6 block-scoping with 'let'
for (let i = 0; i < 5; i++) {
  // 'i' exists only in this block
}
console.log(i); // ReferenceError: i is not defined

// Constant values with 'const'
const PI = 3.14159;
PI = 3; // TypeError: Assignment to constant variable

These simple yet powerful additions significantly reduced a whole class of common bugs and made code easier to reason about.

After for (let i = 0; i < 5; i++) {} finishes, what does console.log(i) do?

Arrow Functions: Solving the 'this' Problem#

Arrow functions are more than shorter syntax (sugar that only saves typing). They address one of JavaScript's most notorious pain points: the value of this, the object a function is working on, depends on how the function is called, not where it is written. Inside a plain callback handed to setInterval, this is no longer your object:

// Pre-ES6: The 'this' problem
function Counter() {
  this.count = 0;
  setInterval(function() {
    this.count++; // 'this' is the window (in a browser), not the Counter!
    console.log(this.count); // NaN
  }, 1000);
}

An arrow function has no this of its own. It uses the this of the code around it, which is called lexical this ("lexical" just means "decided by where the code is written"):

// ES6: Lexical 'this' with arrow functions
function Counter() {
  this.count = 0;
  setInterval(() => {
    this.count++; // 'this' is the Counter instance
    console.log(this.count); // 1, 2, 3...
  }, 1000);
}

(Call either one with new Counter().) Lexical this eliminated countless bugs and verbose workarounds (like var self = this), making callback-heavy code dramatically cleaner.

This interactive example demonstrates the classic problem with this in callbacks and timers and shows how an arrow function resolves it. Explore the code to see how the lexical binding of this leads to more intuitive and less error-prone code.

Arrow Functions and Lexical this
---html
<h1>Arrow Functions and Lexical <code>this</code></h1>
  
  <div class="container">
    <p>One of ES6's most significant improvements was the introduction of arrow functions. Beyond just providing a more concise syntax, arrow functions solve the notorious "this problem" in JavaScript through lexical binding.</p>
  </div>
  
  <div class="tabs">
    <div class="tab active" data-tab="counter">Counter Demo</div>
    <div class="tab" data-tab="syntax">Syntax Examples</div>
    <div class="tab" data-tab="comparison">Side-by-Side Comparison</div>
  </div>
  
  <div class="tab-content active" id="counter">
    <h2>The Counter Problem</h2>
    <p>A classic example of the "this problem" in JavaScript is a counter with a timer. Run both implementations to see the difference.</p>
    
    <div class="demo-container">
      <div class="demo-section">
        <h3>Traditional Function</h3>
        <div class="code-block">function TraditionalCounter() {
  this.count = 0;
  this.display = document.getElementById('traditional-count');
  this.display.textContent = this.count;
  this.running = false;
  
  this.start = function() {
    if (this.running) return;
    
    this.running = true;
    this.interval = setInterval(function() {
      // 'this' is lost here! It refers to window
      // or is undefined in strict mode
      this.count++; // Error!
      this.display.textContent = this.count;
    }, 1000);
  };
  
  this.stop = function() {
    clearInterval(this.interval);
    this.running = false;
  };
}</div>
        <p>This version <span class="highlight">will fail</span> because 'this' inside the setInterval callback refers to the global object.</p>
        <div class="counter-display" id="traditional-count">0</div>
        <div>
          <button id="traditional-start">Start Counter</button>
          <button id="traditional-stop" class="secondary">Stop Counter</button>
        </div>
        <div class="result-panel" id="traditional-result"></div>
        <button class="clear-btn" id="traditional-clear">Clear Log</button>
      </div>
      
      <div class="demo-section">
        <h3>Arrow Function</h3>
        <div class="code-block">function ArrowCounter() {
  this.count = 0;
  this.display = document.getElementById('arrow-count');
  this.display.textContent = this.count;
  this.running = false;
  
  this.start = function() {
    if (this.running) return;
    
    this.running = true;
    this.interval = setInterval(() => {
      // 'this' is lexically bound to ArrowCounter
      this.count++; // Works!
      this.display.textContent = this.count;
    }, 1000);
  };
  
  this.stop = function() {
    clearInterval(this.interval);
    this.running = false;
  };
}</div>
        <p>This version <span class="highlight">works correctly</span> because arrow functions inherit 'this' from their surrounding scope.</p>
        <div class="counter-display" id="arrow-count">0</div>
        <div>
          <button id="arrow-start">Start Counter</button>
          <button id="arrow-stop" class="secondary">Stop Counter</button>
        </div>
        <div class="result-panel" id="arrow-result"></div>
        <button class="clear-btn" id="arrow-clear">Clear Log</button>
      </div>
    </div>
  </div>
  
  <div class="tab-content" id="syntax">
    <h2>Arrow Function Syntax Examples</h2>
    <p>Arrow functions offer multiple syntax variants for different situations:</p>
    
    <div class="code-block">// Traditional function expression
const double1 = function(x) {
  return x * 2;
};

// Arrow function with body and explicit return
const double2 = (x) => {
  return x * 2;
};

// Arrow function with implicit return (no curly braces)
const double3 = (x) => x * 2;

// Single parameter (parentheses optional)
const double4 = x => x * 2;

// Multiple parameters (require parentheses)
const multiply = (a, b) => a * b;

// No parameters (require empty parentheses)
const random = () => Math.random();

// Returning an object literal (wrap in parentheses)
const makePerson = (name, age) => ({ name, age });

// Immediately Invoked Function Expression (IIFE)
const result = ((x) => x * 2)(4); // 8</div>

    <h3>Try It Yourself</h3>
    <div>
      <button id="run-syntax">Run Examples</button>
    </div>
    <div class="result-panel" id="syntax-result"></div>
    <button class="clear-btn" id="syntax-clear">Clear Log</button>
  </div>
  
  <div class="tab-content" id="comparison">
    <h2>Lexical 'this' vs. Dynamic 'this'</h2>
    
    <div class="demo-container">
      <div class="demo-section">
        <h3>Pre-ES6 Workarounds</h3>
        <p>Before arrow functions, developers used various workarounds for the 'this' problem:</p>
        
        <div class="code-block">// 1. Self/That pattern
function Counter() {
  this.count = 0;
  var self = this; // Store reference to 'this'
  
  setInterval(function() {
    self.count++; // Use stored reference
    console.log(self.count);
  }, 1000);
}

// 2. Bind method
function Counter() {
  this.count = 0;
  
  setInterval(function() {
    this.count++; // 'this' is bound correctly
    console.log(this.count);
  }.bind(this), 1000);
}

// 3. Function.prototype.call or .apply
function Counter() {
  this.count = 0;
  
  function increment() {
    this.count++;
    console.log(this.count);
  }
  
  setInterval(function() {
    increment.call(this);
  }.bind(this), 1000);
}</div>
        <button id="run-pre-es6">Run Examples</button>
        <div class="result-panel" id="pre-es6-result"></div>
        <button class="clear-btn" id="pre-es6-clear">Clear Log</button>
      </div>
      
      <div class="demo-section">
        <h3>When Not To Use Arrow Functions</h3>
        <p>Arrow functions aren't always the best choice:</p>
        
        <div class="code-block">// 1. Object methods (when 'this' should be object)
const person = {
  name: 'Alice',
  // BAD: 'this' is not bound to person
  sayHi: () => {
    console.log(`Hi, I'm ${this.name}`); // 'this' is window/undefined
  },
  // GOOD: 'this' is bound to person
  greet() {
    console.log(`Hello, I'm ${this.name}`);
  }
};

// 2. Constructor functions
// BAD: Arrow functions cannot be used as constructors
const Person = (name) => {
  this.name = name; // 'this' is wrong here
};

// 3. Event handlers that need access to the element
// BAD: 'this' will not be the button
button.addEventListener('click', () => {
  this.classList.toggle('active'); // 'this' is window/undefined
});

// GOOD: 'this' will be the button
button.addEventListener('click', function() {
  this.classList.toggle('active');
});</div>
        <button id="run-non-arrow">Run Examples</button>
        <div class="result-panel" id="non-arrow-result"></div>
        <button class="clear-btn" id="non-arrow-clear">Clear Log</button>
      </div>
    </div>
  </div>
---css
:root {
      --bg-color: #f8f9fa;
      --text-color: #212529;
      --primary-color: #6c5ce7;
      --secondary-color: #e17055;
      --accent-color: #00b894;
      --light-bg: #ffffff;
      --border-color: #dee2e6;
      --code-bg: #f1f1f1;
    }

    @media (prefers-color-scheme: dark) {
      :root {
        --bg-color: #1a1a2e;
        --text-color: #e6e6e6;
        --primary-color: #8c7ae6;
        --secondary-color: #e67e22;
        --accent-color: #00cec9;
        --light-bg: #282a36;
        --border-color: #4a4a59;
        --code-bg: #2d2d3a;
      }
    }

    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      background-color: var(--bg-color);
      color: var(--text-color);
      line-height: 1.6;
      padding: 20px;
      max-width: 800px;
      margin: 0 auto;
      transition: all 0.3s ease;
    }

    h1, h2, h3 {
      color: var(--primary-color);
    }

    h1 {
      text-align: center;
      margin-bottom: 30px;
    }

    .container {
      background-color: var(--light-bg);
      border-radius: 10px;
      padding: 20px;
      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
      margin-bottom: 20px;
    }

    .tabs {
      display: flex;
      border-bottom: 2px solid var(--border-color);
      margin-bottom: 20px;
    }

    .tab {
      padding: 10px 20px;
      cursor: pointer;
      border-bottom: 3px solid transparent;
      transition: all 0.3s;
      font-weight: bold;
    }

    .tab.active {
      color: var(--primary-color);
      border-bottom: 3px solid var(--primary-color);
    }

    .tab:hover:not(.active) {
      border-bottom: 3px solid var(--border-color);
    }

    .tab-content {
      display: none;
    }

    .tab-content.active {
      display: block;
      animation: fadeIn 0.5s;
    }

    @keyframes fadeIn {
      from { opacity: 0; }
      to { opacity: 1; }
    }

    .demo-container {
      display: flex;
      gap: 20px;
      margin-top: 20px;
      flex-wrap: wrap;
    }

    .demo-section {
      flex: 1;
      min-width: 300px;
      background-color: var(--light-bg);
      border-radius: 8px;
      padding: 15px;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
      border: 1px solid var(--border-color);
    }

    .code-block {
      background-color: var(--code-bg);
      border-radius: 4px;
      padding: 15px;
      font-family: 'Courier New', Courier, monospace;
      font-size: 14px;
      overflow-x: auto;
      margin-bottom: 15px;
      white-space: pre;
    }

    button {
      background-color: var(--primary-color);
      color: white;
      border: none;
      padding: 8px 15px;
      border-radius: 4px;
      cursor: pointer;
      transition: background-color 0.3s;
      font-size: 14px;
    }

    button.secondary {
      background-color: var(--secondary-color);
    }

    button:hover {
      opacity: 0.9;
    }

    .result-panel {
      background-color: var(--code-bg);
      border-radius: 4px;
      padding: 10px;
      margin-top: 10px;
      font-family: 'Courier New', Courier, monospace;
      min-height: 100px;
      max-height: 200px;
      overflow-y: auto;
    }

    .clear-btn {
      background-color: var(--border-color);
      color: var(--text-color);
      font-size: 12px;
      padding: 4px 8px;
      margin-top: 5px;
    }

    .counter-display {
      font-size: 24px;
      font-weight: bold;
      text-align: center;
      margin: 15px 0;
      color: var(--primary-color);
    }

    .explanation {
      margin-top: 10px;
      background-color: rgba(var(--primary-color-rgb), 0.1);
      padding: 10px;
      border-radius: 4px;
      font-style: italic;
    }

    .highlight {
      color: var(--secondary-color);
      font-weight: bold;
    }

    :root {
      --primary-color-rgb: 108, 92, 231;
    }

    @media (prefers-color-scheme: dark) {
      :root {
        --primary-color-rgb: 140, 122, 230;
      }
    }
---js
// Tab switching
const tabs = document.querySelectorAll('.tab');
const tabContents = document.querySelectorAll('.tab-content');

tabs.forEach(tab => {
  tab.addEventListener('click', () => {
    const tabId = tab.getAttribute('data-tab');

    // Update active tab
    tabs.forEach(t => t.classList.remove('active'));
    tab.classList.add('active');

    // Show content
    tabContents.forEach(content => {
      content.classList.remove('active');
      if (content.id === tabId) {
        content.classList.add('active');
      }
    });
  });
});

// Output functions
function logToElement(element, message) {
  const line = document.createElement('div');
  line.textContent = message;
  element.appendChild(line);
  element.scrollTop = element.scrollHeight;
}

// Clear log buttons
document.querySelectorAll('.clear-btn').forEach(btn => {
  btn.addEventListener('click', () => {
    const resultId = btn.id.replace('-clear', '-result');
    document.getElementById(resultId).innerHTML = '';
  });
});

// Counter demos
const traditionalCountDisplay = document.getElementById('traditional-count');
const arrowCountDisplay = document.getElementById('arrow-count');
const traditionalResult = document.getElementById('traditional-result');
const arrowResult = document.getElementById('arrow-result');

// Traditional counter (purposely broken)
function TraditionalCounter() {
  this.count = 0;
  this.running = false;

  this.start = function () {
    if (this.running) return;

    logToElement(traditionalResult, "Starting traditional counter...");
    this.running = true;

    try {
      this.interval = setInterval(function () {
        // 'this' is lost here - it refers to window or is undefined in strict mode
        this.count++;
        logToElement(traditionalResult, `Count: ${this.count}`);

        // This will cause an error since this.count is undefined
        traditionalCountDisplay.textContent = this.count;
      }, 1000);
    } catch (error) {
      logToElement(traditionalResult, `Error: ${error.message}`);
    }
  };

  this.stop = function () {
    clearInterval(this.interval);
    this.running = false;
    logToElement(traditionalResult, "Traditional counter stopped.");
  };
}

// Arrow function counter (works correctly)
function ArrowCounter() {
  this.count = 0;
  this.running = false;

  this.start = function () {
    if (this.running) return;

    logToElement(arrowResult, "Starting arrow counter...");
    this.running = true;

    this.interval = setInterval(() => {
      // 'this' is correctly bound to ArrowCounter
      this.count++;
      arrowCountDisplay.textContent = this.count;
      logToElement(arrowResult, `Count: ${this.count}`);
    }, 1000);
  };

  this.stop = function () {
    clearInterval(this.interval);
    this.running = false;
    logToElement(arrowResult, "Arrow counter stopped.");
  };
}

const traditionalCounter = new TraditionalCounter();
const arrowCounter = new ArrowCounter();

// Event listeners for counter buttons
document.getElementById('traditional-start').addEventListener('click', function () {
  traditionalCounter.start();
});

document.getElementById('traditional-stop').addEventListener('click', function () {
  traditionalCounter.stop();
});

document.getElementById('arrow-start').addEventListener('click', function () {
  arrowCounter.start();
});

document.getElementById('arrow-stop').addEventListener('click', function () {
  arrowCounter.stop();
});

// Syntax examples
const syntaxResult = document.getElementById('syntax-result');
document.getElementById('run-syntax').addEventListener('click', function () {
  syntaxResult.innerHTML = '';

  // Traditional function expression
  const double1 = function (x) {
    return x * 2;
  };
  logToElement(syntaxResult, `double1(5): ${double1(5)}`);

  // Arrow function with body
  const double2 = x => {
    return x * 2;
  };
  logToElement(syntaxResult, `double2(5): ${double2(5)}`);

  // Arrow function with implicit return
  const double3 = x => x * 2;
  logToElement(syntaxResult, `double3(5): ${double3(5)}`);

  // Single parameter
  const double4 = x => x * 2;
  logToElement(syntaxResult, `double4(5): ${double4(5)}`);

  // Multiple parameters
  const multiply = (a, b) => a * b;
  logToElement(syntaxResult, `multiply(4, 6): ${multiply(4, 6)}`);

  // No parameters
  const random = () => Math.random();
  logToElement(syntaxResult, `random(): ${random()}`);

  // Returning object literal
  const makePerson = (name, age) => ({ name, age });
  logToElement(syntaxResult, `makePerson('Alice', 30): ${JSON.stringify(makePerson('Alice', 30))}`);

  // IIFE
  const result = (x => x * 2)(4);
  logToElement(syntaxResult, `IIFE result: ${result}`);
});

// Pre-ES6 examples
const preEs6Result = document.getElementById('pre-es6-result');
document.getElementById('run-pre-es6').addEventListener('click', function () {
  preEs6Result.innerHTML = '';

  // Examples just to demonstrate in console
  logToElement(preEs6Result, "Self/That Pattern:");

  function CounterWithSelf() {
    this.count = 0;
    var self = this; // Store reference

    // Simulating setInterval for demo
    self.count++;
    logToElement(preEs6Result, `  Count using self: ${self.count}`);
  }
  new CounterWithSelf();

  logToElement(preEs6Result, "\nBind Method:");

  function CounterWithBind() {
    this.count = 0;

    // Simulating the bind approach
    const increment = function () {
      this.count++;
      logToElement(preEs6Result, `  Count using bind: ${this.count}`);
    }.bind(this);

    increment();
  }
  new CounterWithBind();

  logToElement(preEs6Result, "\nCall Method:");

  function CounterWithCall() {
    this.count = 0;

    function increment() {
      this.count++;
      logToElement(preEs6Result, `  Count using call: ${this.count}`);
    }

    // Using call to set 'this'
    increment.call(this);
  }
  new CounterWithCall();
});

// Non-arrow examples
const nonArrowResult = document.getElementById('non-arrow-result');
document.getElementById('run-non-arrow').addEventListener('click', function () {
  nonArrowResult.innerHTML = '';

  // Object methods
  logToElement(nonArrowResult, "Object Methods:");

  const person = {
    name: 'Alice',
    // Arrow function (incorrect)
    sayHi: () => {
      logToElement(nonArrowResult, `  Arrow method: Hi, I'm ${this.name || 'undefined'}`);
    },
    // Regular method (correct)
    greet() {
      logToElement(nonArrowResult, `  Regular method: Hello, I'm ${this.name}`);
    } };


  person.sayHi();
  person.greet();

  // Constructor example
  logToElement(nonArrowResult, "\nConstructors:");

  try {
    // This will fail
    const Person = name => {
      this.name = name;
    };
    const alice = new Person('Alice');
    logToElement(nonArrowResult, `  Created: ${alice.name}`);
  } catch (e) {
    logToElement(nonArrowResult, `  Error: Arrow functions cannot be used as constructors`);
  }

  // Regular constructor
  function PersonRegular(name) {
    this.name = name;
  }
  const bob = new PersonRegular('Bob');
  logToElement(nonArrowResult, `  Regular constructor: ${bob.name}`);

  // Event handlers
  logToElement(nonArrowResult, "\nEvent Handlers:");
  logToElement(nonArrowResult, "  For event handlers on DOM elements:");
  logToElement(nonArrowResult, "  - Arrow: 'this' refers to the surrounding scope");
  logToElement(nonArrowResult, "  - Regular: 'this' refers to the element that triggered the event");
});

Coding challenge: Fix the Timer

This Timer class should count 1, 2, 3 and then stop, but the callback loses this. Fix it, then fix it a second way.

Expected result: the console shows tick 1, tick 2, tick 3 instead of tick NaN. Once with an arrow function, then again with .bind(this).

Fix the Timer
---js
class Timer {
  constructor() {
    this.count = 0;
  }

  start() {
    // TODO: this callback loses `this`, so count becomes NaN.
    // Fix 1: make it an arrow function.
    // Fix 2: keep `function () {...}` and add .bind(this) after it.
    this.id = setInterval(function () {
      this.count++;
      console.log("tick", this.count);
    }, 300);

    setTimeout(() => clearInterval(this.id), 1000);
  }
}

new Timer().start();
Why does this.count++ give NaN inside setInterval(function () { ... })?

Template Literals: The End of String Concatenation Nightmares#

Remember the days of building complex strings with concatenation?

// Pre-ES6 string concatenation
var oldName = "Alice";
var oldGreeting = "Hello, " + oldName + "!\n" +
                  "Welcome to our site.";
// ES6 template literals
const name = "Alice";
const greeting = `Hello, ${name}!
Welcome to our site.`;
console.log(greeting);
// Hello, Alice!
// Welcome to our site.

Template literals (strings in backticks) brought simple string interpolation, where ${...} drops a value into the text, and multi-line strings to JavaScript, making text manipulation far more intuitive.

Beyond that, they offer a more advanced trick called tagged templates: a function placed in front of the backticks receives the text pieces and values separately, so it can process them (escaping HTML, for example). You will not need that today. This interactive workshop lets you experiment with different uses of template literals – from basic variable insertion to building complex HTML templates. Modify the code examples to see how template literals can transform string manipulation in your applications.

Template Literals Workshop
---html
<div class="container">
	<h1>Template Literals Workshop</h1>

	<div class="intro">
		<p>Template literals, introduced in ES6, revolutionized string handling in JavaScript. They provide a more elegant way to create strings with dynamic content, support multi-line strings without hacks, and even enable powerful string processing through tagged templates.</p>
		<p>This interactive workshop lets you experiment with different aspects of template literals. Try the examples, modify the code, and see the results instantly!</p>
	</div>

	<div class="workshop-grid">
		<div class="workshop-section">
			<h3 class="section-title">Basic String Interpolation</h3>

			<div class="example-block">
				<div class="code-header">
					<span class="code-title">Simple Variable Insertion</span>
				</div>
				<div class="code-container">
					<textarea class="code-editor" id="basic-editor">const name = 'Alice';
const age = 28;

// Old way with concatenation
const oldWay = 'Hello, my name is ' + name + ' and I am ' + age + ' years old.';

// New way with template literals
const newWay = `Hello, my name is ${name} and I am ${age} years old.`;

// Return both for display
return { oldWay, newWay };</textarea>
					<div class="code-output" id="basic-output"></div>
				</div>
				<button class="run-button" data-editor="basic-editor" data-output="basic-output">Run Code</button>
				<button class="reset-button" data-editor="basic-editor">Reset</button>
				<div class="help-text">Try changing the values of <code>name</code> and <code>age</code>, or add more variables to interpolate.</div>
			</div>
		</div>

		<div class="workshop-section">
			<h3 class="section-title">Multi-line Strings</h3>

			<div class="example-block">
				<div class="code-header">
					<span class="code-title">Creating Multi-line Output</span>
				</div>
				<div class="code-container">
					<textarea class="code-editor" id="multiline-editor">// Old way with concatenation or escape characters
const oldWay = 'This is line one.\n' +
'This is line two.\n' +
'This is line three.';

// New way with template literals
const newWay = `This is line one.
This is line two.
This is line three.`;

return { oldWay, newWay };</textarea>
					<div class="code-output" id="multiline-output"></div>
				</div>
				<button class="run-button" data-editor="multiline-editor" data-output="multiline-output">Run Code</button>
				<button class="reset-button" data-editor="multiline-editor">Reset</button>
				<div class="help-text">Try adding more lines or changing the content to see how line breaks are preserved.</div>
			</div>
		</div>

		<div class="workshop-section">
			<h3 class="section-title">Expressions in Templates</h3>

			<div class="example-block">
				<div class="code-header">
					<span class="code-title">Using Expressions and Functions</span>
				</div>
				<div class="code-container">
					<textarea class="code-editor" id="expressions-editor">const price = 19.99;
const quantity = 3;

// You can put any expression inside ${}
const total = `Total: $${(price * quantity).toFixed(2)}`;

// Function calls work too
function calculateTax(amount) {
  return amount * 0.08; // 8% tax
}

const receipt = `
Product: Widget
Price: $${price}
Quantity: ${quantity}
Subtotal: $${(price * quantity).toFixed(2)}
Tax: $${calculateTax(price * quantity).toFixed(2)}
Total: $${(price * quantity + calculateTax(price * quantity)).toFixed(2)}
`;

return { total, receipt };</textarea>
					<div class="code-output" id="expressions-output"></div>
				</div>
				<button class="run-button" data-editor="expressions-editor" data-output="expressions-output">Run Code</button>
				<button class="reset-button" data-editor="expressions-editor">Reset</button>
				<div class="help-text">Try changing the price, quantity, or tax rate. Add a discount calculation of your own!</div>
			</div>
		</div>

		<div class="workshop-section">
			<h3 class="section-title">Nesting Templates</h3>

			<div class="example-block">
				<div class="code-header">
					<span class="code-title">Template Literals Inside Template Literals</span>
				</div>
				<div class="code-container">
					<textarea class="code-editor" id="nesting-editor">const users = [
  { name: 'Alice', role: 'Developer', projects: ['API', 'Dashboard'] },
  { name: 'Bob', role: 'Designer', projects: ['UI Redesign', 'Logo'] },
  { name: 'Charlie', role: 'Manager', projects: ['Planning', 'Hiring'] }
];

const teamReport = `
Team Members:
${users.map(user => `
  - ${user.name} (${user.role})
    Projects: ${user.projects.map(proj => `"${proj}"`).join(', ')}
`).join('')}
`;

return { teamReport };</textarea>
					<div class="code-output" id="nesting-output"></div>
				</div>
				<button class="run-button" data-editor="nesting-editor" data-output="nesting-output">Run Code</button>
				<button class="reset-button" data-editor="nesting-editor">Reset</button>
				<div class="help-text">Try adding more users or additional properties to each user object.</div>
			</div>
		</div>

		<div class="workshop-section">
			<h3 class="section-title">Tagged Templates</h3>

			<div class="example-block">
				<div class="code-header">
					<span class="code-title">Custom String Processing</span>
				</div>
				<div class="code-container">
					<textarea class="code-editor" id="tagged-editor">// Tagged templates allow custom processing of template literals
function highlight(strings, ...values) {
  let result = '';
  
  // Interleave the strings with the values
  strings.forEach((string, i) => {
    result += string;
    
    // Add the wrapped value if there is one
    if (i < values.length) {
      result += `<span style="color: red; font-weight: bold;">${values[i]}</span>`;
    }
  });
  
  return result;
}

const name = 'JavaScript';
const feature = 'template literals';
const year = 2015;

// Using the tag function
const message = highlight`${name} introduced ${feature} in ${year}!`;

// Another example tag function
function currency(strings, ...values) {
  let result = '';
  
  strings.forEach((string, i) => {
    result += string;
    
    // Format numbers as USD currency
    if (i < values.length) {
      if (typeof values[i] === 'number') {
        result += `$${values[i].toFixed(2)}`;
      } else {
        result += values[i];
      }
    }
  });
  
  return result;
}

const price = 29.99;
const shipping = 4.99;
const priceMessage = currency`Your order costs ${price} plus shipping of ${shipping}.`;

return { message, priceMessage };</textarea>
					<div class="code-output" id="tagged-output"></div>
				</div>
				<button class="run-button" data-editor="tagged-editor" data-output="tagged-output">Run Code</button>
				<button class="reset-button" data-editor="tagged-editor">Reset</button>
				<div class="help-text">Try modifying the highlight function to use different colors or formatting.</div>

				<div class="bonus-challenge">
					<h4>Bonus Challenge:</h4>
					<p>Create a new tag function that converts temperatures from Celsius to Fahrenheit!</p>
				</div>
			</div>
		</div>

		<div class="workshop-section">
			<h3 class="section-title">Real-World Example: HTML Templates</h3>

			<div class="example-block">
				<div class="code-header">
					<span class="code-title">Creating Dynamic HTML</span>
				</div>
				<div class="code-container">
					<textarea class="code-editor" id="html-editor">// Define tag function for safe HTML
function html(strings, ...values) {
  // Simple HTML escaping for demonstration
  function escapeHTML(str) {
    return String(str)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#039;');
  }
  
  let result = '';
  strings.forEach((string, i) => {
    result += string;
    if (i < values.length) {
      // Escape values to prevent XSS
      if (typeof values[i] === 'string') {
        result += escapeHTML(values[i]);
      } else {
        result += escapeHTML(String(values[i]));
      }
    }
  });
  
  return result;
}

// Sample product data
const product = {
  name: 'ES6 in Action',
  description: 'Master modern JavaScript',
  price: 39.99,
  inStock: true,
  tags: ['programming', 'javascript', 'es6']
};

// Create HTML template
const productCard = html`
<div class="product-card">
  <h2>${product.name}</h2>
  <p>${product.description}</p>
  <div class="price">$${product.price}</div>
  <div class="status">${product.inStock ? 'In Stock' : 'Out of Stock'}</div>
  <div class="tags">
    ${product.tags.map(tag => `<span class="tag">${tag}</span>`).join(' ')}
  </div>
</div>
`;

// Return the HTML (for display only)
return { productCard };</textarea>
					<div class="code-output" id="html-output"></div>
				</div>
				<button class="run-button" data-editor="html-editor" data-output="html-output">Run Code</button>
				<button class="reset-button" data-editor="html-editor">Reset</button>
				<div class="help-text">Try adding more product features or changing the HTML structure.</div>
			</div>
		</div>
	</div>

	<div class="footer">
		<p>This interactive example demonstrates how ES6 template literals make string manipulation in JavaScript more powerful and expressive.</p>
	</div>
</div>
---css
:root {
	--bg-color: #f5f5f5;
	--text-color: #333;
	--primary-color: #4a6fa5;
	--secondary-color: #6a9c78;
	--accent-color: #c25450;
	--light-bg: #ffffff;
	--border-color: #ddd;
	--shadow-color: rgba(0, 0, 0, 0.1);
	--code-bg: #f8f8f8;
	--output-bg: #f3f3f3;
}

@media (prefers-color-scheme: dark) {
	:root {
		--bg-color: #282c34;
		--text-color: #abb2bf;
		--primary-color: #61afef;
		--secondary-color: #98c379;
		--accent-color: #e06c75;
		--light-bg: #333842;
		--border-color: #5c6370;
		--shadow-color: rgba(0, 0, 0, 0.3);
		--code-bg: #21252b;
		--output-bg: #3e4451;
	}
}

body {
	font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
	background-color: var(--bg-color);
	color: var(--text-color);
	line-height: 1.6;
	padding: 20px;
	transition: background-color 0.3s, color 0.3s;
	margin: 0;
}

.container {
	max-width: 1000px;
	margin: 0 auto;
}

h1,
h2,
h3,
h4 {
	color: var(--primary-color);
}

h1 {
	text-align: center;
	margin-bottom: 30px;
	font-size: 2.2rem;
}

.intro {
	background-color: var(--light-bg);
	padding: 20px;
	border-radius: 8px;
	box-shadow: 0 2px 5px var(--shadow-color);
	margin-bottom: 25px;
}

.workshop-grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
	gap: 25px;
	margin-bottom: 25px;
}

.workshop-section {
	background-color: var(--light-bg);
	border-radius: 8px;
	padding: 20px;
	box-shadow: 0 2px 5px var(--shadow-color);
}

.section-title {
	color: var(--primary-color);
	margin-top: 0;
	padding-bottom: 10px;
	border-bottom: 1px solid var(--border-color);
}

.example-block {
	margin-bottom: 15px;
}

.code-container {
	position: relative;
}

.code-header {
	display: flex;
	justify-content: space-between;
	align-items: center;
	margin-bottom: 5px;
}

.code-title {
	font-size: 0.9rem;
	color: var(--secondary-color);
	font-weight: bold;
}

.code-editor,
.code-output {
	background-color: var(--code-bg);
	border: 1px solid var(--border-color);
	border-radius: 4px;
	padding: 12px;
	font-family: "Courier New", Courier, monospace;
	font-size: 0.9rem;
	line-height: 1.5;
	width: 100%;
	box-sizing: border-box;
}

textarea {
	color: var(--text-color)
}

.code-editor {
	min-height: 100px;
	resize: vertical;
}

.code-output {
	background-color: var(--output-bg);
	min-height: 50px;
	margin-top: 10px;
	white-space: pre-wrap;
	overflow-wrap: break-word;
}

.run-button {
	background-color: var(--secondary-color);
	color: white;
	border: none;
	padding: 5px 10px;
	border-radius: 4px;
	cursor: pointer;
	font-size: 0.85rem;
	transition: background-color 0.2s;
}

.run-button:hover {
	opacity: 0.9;
}

.reset-button {
	background-color: var(--accent-color);
	color: white;
	border: none;
	padding: 5px 10px;
	border-radius: 4px;
	cursor: pointer;
	font-size: 0.85rem;
	transition: background-color 0.2s;
	margin-left: 8px;
}

.reset-button:hover {
	opacity: 0.9;
}

.footer {
	text-align: center;
	margin-top: 30px;
	padding-top: 10px;
	border-top: 1px solid var(--border-color);
	font-size: 0.9rem;
}

.highlight {
	color: var(--accent-color);
	font-weight: bold;
}

.help-text {
	font-size: 0.9rem;
	margin-top: 10px;
	font-style: italic;
}

.bonus-challenge {
	background-color: rgba(106, 156, 120, 0.1);
	border-left: 3px solid var(--secondary-color);
	padding: 10px 15px;
	margin-top: 15px;
}

@media (prefers-color-scheme: dark) {
	.bonus-challenge {
		background-color: rgba(152, 195, 121, 0.1);
	}
}

@media (max-width: 768px) {
	.workshop-grid {
		grid-template-columns: 1fr;
	}
}
---js
// Store original code for each editor
const originalCode = {};
document.querySelectorAll(".code-editor").forEach(editor => {
  originalCode[editor.id] = editor.value;
});

// Run button click handlers
document.querySelectorAll(".run-button").forEach(button => {
  button.addEventListener("click", function () {
    const editorId = this.getAttribute("data-editor");
    const outputId = this.getAttribute("data-output");
    const editor = document.getElementById(editorId);
    const output = document.getElementById(outputId);

    try {
      // Create a safe execution environment
      const executeCode = code => {
        // Create a function from the code that returns the result
        return Function(`
              "use strict";
              try {
                ${code}
              } catch (error) {
                return { error: error.message };
              }
            `)();
      };

      // Execute the code
      const result = executeCode(editor.value);

      // Format and display the result
      if (result && result.error) {
        output.textContent = `Error: ${result.error}`;
      } else if (typeof result === "object") {
        let formattedResult = "";

        // Handle HTML output specially to show the HTML tags
        for (const [key, value] of Object.entries(result)) {
          if (key === "message" || key === "productCard") {
            // For HTML content, show both the raw HTML and rendered version
            formattedResult += `${key} (Raw HTML):\n${value}\n\n`;

            // Create a div to show rendered version
            const renderedDiv = document.createElement("div");
            renderedDiv.innerHTML = `<div style="margin-top:10px;padding:10px;border:1px solid var(--border-color);border-radius:4px;">${value}</div>`;

            // Add rendered version after this run is complete
            setTimeout(() => {
              output.appendChild(renderedDiv);
            }, 0);
          } else {
            formattedResult += `${key}:\n${value}\n\n`;
          }
        }

        output.textContent = formattedResult;
      } else {
        output.textContent = String(result);
      }
    } catch (error) {
      output.textContent = `Execution Error: ${error.message}`;
    }
  });
});

// Reset button click handlers
document.querySelectorAll(".reset-button").forEach(button => {
  button.addEventListener("click", function () {
    const editorId = this.getAttribute("data-editor");
    const editor = document.getElementById(editorId);
    editor.value = originalCode[editorId];

    // Also clear the corresponding output
    const outputId = document.
    querySelector(`[data-editor="${editorId}"]`).
    getAttribute("data-output");
    document.getElementById(outputId).textContent = "";
    document.getElementById(outputId).innerHTML = "";
  });
});

// Run all examples on page load
window.addEventListener("DOMContentLoaded", () => {
  setTimeout(() => {
    document.querySelectorAll(".run-button").forEach(button => {
      button.click();
    });
  }, 500);
});

Destructuring: Extracting What You Need#

Destructuring assignment provided elegant ways to extract values from complex data structures:

// Pre-ES6 object property access
var oldUser = {name: "Alice", age: 30};
var oldName = oldUser.name;
var oldAge = oldUser.age;
// ES6 destructuring
const user = {name: "Alice", age: 30};
const {name, age} = user;
console.log(name, age); // Alice 30

// Array destructuring
const [first, second] = [1, 2, 3];
console.log(first, second); // 1 2

// Function parameter destructuring
function displayUser({name, age}) {
  console.log(`${name} is ${age} years old`);
}
displayUser(user); // Alice is 30 years old

This feature dramatically reduced boilerplate code, especially when working with complex APIs or state management.

Given const {name, age} = {name: "Alice", age: 30};, what does console.log(age) print?

Try the interactive explorer below to see various destructuring patterns in action. Experiment with the code examples to understand how destructuring can simplify data extraction in your own projects.

ES6 Destructuring Explorer
---html
<h1>ES6 Destructuring Explorer</h1>

<div class="description">
	<p>Destructuring in ES6 allows you to extract data from arrays and objects with elegant syntax. Select an example to see it in action.</p>
</div>

<select id="example-select" class="example-select">
	<option value="basic-object">Basic Object Destructuring</option>
	<option value="nested-object">Nested Object Destructuring</option>
	<option value="array">Array Destructuring</option>
	<option value="function-params">Function Parameter Destructuring</option>
	<option value="mixed">Mixed Destructuring</option>
</select>

<div class="container">
	<div class="code-section">
		<h3>Code</h3>
		<pre id="code-display"></pre>
		<button id="run-button">Run Code</button>
	</div>
	<div class="result-section">
		<h3>Result</h3>
		<pre id="result-display"></pre>
	</div>
</div>
---css
:root {
	--bg-color: #f5f5f5;
	--text-color: #333;
	--accent-color: #4a6da7;
	--code-bg: #e0e0e0;
	--result-bg: #fff;
	--border-color: #ccc;
}

@media (prefers-color-scheme: dark) {
	:root {
		--bg-color: #1e1e1e;
		--text-color: #e0e0e0;
		--accent-color: #5b8ad6;
		--code-bg: #2d2d2d;
		--result-bg: #2a2a2a;
		--border-color: #444;
	}
}

body {
	font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
	background-color: var(--bg-color);
	color: var(--text-color);
	line-height: 1.6;
	padding: 20px;
	max-width: 800px;
	margin: 0 auto;
	transition: background-color 0.3s, color 0.3s;
}

h1 {
	color: var(--accent-color);
	border-bottom: 2px solid var(--accent-color);
	padding-bottom: 10px;
}

.container {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: 20px;
	margin-top: 20px;
}

@media (max-width: 700px) {
	.container {
		grid-template-columns: 1fr;
	}
}

.code-section,
.result-section {
	border: 1px solid var(--border-color);
	border-radius: 5px;
	padding: 15px;
}

.code-section {
	background-color: var(--code-bg);
}

.result-section {
	background-color: var(--result-bg);
}

pre {
	margin: 0;
	white-space: pre-wrap;
	word-break: break-word;
}

button {
	background-color: var(--accent-color);
	color: white;
	border: none;
	padding: 8px 15px;
	border-radius: 4px;
	cursor: pointer;
	margin-top: 10px;
	font-size: 14px;
}

button:hover {
	opacity: 0.9;
}

.example-select {
	margin-bottom: 15px;
	padding: 8px;
	border-radius: 4px;
	border: 1px solid var(--border-color);
	background-color: var(--bg-color);
	color: var(--text-color);
}

.description {
	margin-bottom: 15px;
}
---js
// The examples collection with both code and descriptions
const examples = {
  "basic-object": {
    code: `// Basic object destructuring
const user = {
  name: "Alice Johnson",
  age: 28,
  email: "alice@example.com"
};

// Extract properties into variables
const { name, age, email } = user;

// Display the extracted values
console.log("Name:", name);
console.log("Age:", age);
console.log("Email:", email);

// With custom variable names
const { name: userName, email: userContact } = user;
console.log("Username:", userName);
console.log("Contact:", userContact);`,
    description: "Extract properties from objects into individual variables." },

  "nested-object": {
    code: `// Nested object destructuring
const company = {
  name: "TechCorp",
  location: {
    city: "San Francisco",
    state: "CA",
    coordinates: {
      lat: 37.7749,
      lng: -122.4194
    }
  },
  employees: 500
};

// Extract nested properties
const { 
  name, 
  location: { 
    city, 
    state, 
    coordinates: { lat, lng } 
  }, 
  employees 
} = company;

console.log("Company:", name);
console.log("City:", city);
console.log("State:", state);
console.log("Coordinates:", lat, lng);
console.log("Employees:", employees);`,
    description: "Extract deeply nested properties from objects in one statement." },

  array: {
    code: `// Array destructuring
const colors = ["red", "green", "blue", "yellow", "purple"];

// Extract first three items
const [first, second, third] = colors;
console.log("First three colors:", first, second, third);

// Skip elements with empty positions
const [primary, , tertiary, , quinary] = colors;
console.log("Selected colors:", primary, tertiary, quinary);

// Rest pattern to collect remaining items
const [head, ...rest] = colors;
console.log("First color:", head);
console.log("Remaining colors:", rest);

// Swap variables without a temporary variable
let a = 5, b = 10;
console.log("Before swap: a =", a, "b =", b);
[a, b] = [b, a];
console.log("After swap: a =", a, "b =", b);`,
    description:
    "Extract values from arrays and use the rest operator to collect remaining items." },

  "function-params": {
    code: `// Function parameter destructuring
function displayUserProfile({ name, age, location = "Unknown" }) {
  console.log(\`Name: \${name}\`);
  console.log(\`Age: \${age}\`);
  console.log(\`Location: \${location}\`);
}

// Call with an object
const user1 = {
  name: "Bob Smith",
  age: 32
};

const user2 = {
  name: "Carol Davis",
  age: 27,
  location: "Seattle"
};

console.log("User 1:");
displayUserProfile(user1);

console.log("\\nUser 2:");
displayUserProfile(user2);`,
    description:
    "Extract object properties directly in function parameters, with default values." },

  mixed: {
    code: `// Mixed array and object destructuring
const person = {
  name: "David Williams",
  age: 34,
  skills: ["JavaScript", "React", "Node.js"],
  contact: {
    email: "david@example.com",
    phone: "555-1234"
  }
};

// Extract mix of properties and nested items
const { 
  name, 
  skills: [primarySkill, ...otherSkills],
  contact: { email }
} = person;

console.log("Name:", name);
console.log("Primary skill:", primarySkill);
console.log("Other skills:", otherSkills);
console.log("Email:", email);

// Advanced: Destructure array of objects
const team = [
  { id: 1, name: "Alex", role: "Developer" },
  { id: 2, name: "Taylor", role: "Designer" },
  { id: 3, name: "Morgan", role: "Manager" }
];

// Get the second team member's role
const [, { role: designerRole }] = team;
console.log("Designer role:", designerRole);`,
    description:
    "Combine array and object destructuring for complex data extraction." } };



// DOM elements
const codeDisplay = document.getElementById("code-display");
const resultDisplay = document.getElementById("result-display");
const exampleSelect = document.getElementById("example-select");
const runButton = document.getElementById("run-button");

// Function to update the displayed code
function updateCodeDisplay() {
  const selectedExample = exampleSelect.value;
  codeDisplay.textContent = examples[selectedExample].code;
  resultDisplay.textContent = "";
}

// Function to run the code and display results
function runCode() {
  resultDisplay.textContent = "";

  // Create a custom console.log function that outputs to our display
  const originalConsoleLog = console.log;
  console.log = function (...args) {
    // Call the original to keep logging to browser console
    originalConsoleLog.apply(console, args);

    // Format the arguments nicely
    const output = args.
    map(arg => {
      if (typeof arg === "object" && arg !== null) {
        return JSON.stringify(arg);
      }
      return String(arg);
    }).
    join(" ");

    // Append to our display
    resultDisplay.textContent += output + "\n";
  };

  // Execute the code in a try/catch block
  try {
    const selectedExample = exampleSelect.value;
    // Using Function constructor to execute the code
    new Function(examples[selectedExample].code)();
  } catch (error) {
    resultDisplay.textContent += `Error: ${error.message}`;
  } finally {
    // Restore original console.log
    console.log = originalConsoleLog;
  }
}

// Event listeners
exampleSelect.addEventListener("change", updateCodeDisplay);
runButton.addEventListener("click", runCode);

// Initial setup
updateCodeDisplay();

Object and Class Improvements#

Enhanced Object Literals#

ES6 made creating and working with objects more concise:

// Pre-ES6 object creation
function createPersonOld(name, age) {
  return {
    name: name,
    age: age,
    greet: function() {
      return "Hello, " + this.name;
    }
  };
}
// ES6 enhanced object literals
function createPerson(name, age) {
  return {
    name,  // Property shorthand
    age,   // Property shorthand
    greet() {  // Method shorthand
      return `Hello, ${this.name}`;
    },
    ["data-" + age]: age  // Computed property names
  };
}
console.log(createPerson("Ann", 5).greet()); // Hello, Ann
console.log(createPerson("Ann", 5)["data-5"]); // 5

Classes: Making JavaScript More Approachable#

JavaScript's prototype-based inheritance was often confusing, especially for developers coming from class-based languages. ES6 classes provided syntactic sugar (a nicer way to write something the language already could do) over this system, making object-oriented programming more accessible:

// Pre-ES6 prototype-based inheritance
function OldPerson(name) {
  this.name = name;
}
OldPerson.prototype.greet = function() {
  return "Hello, " + this.name;
};
// ES6 class syntax
class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    return `Hello, ${this.name}`;
  }

  static create(name) {
    return new Person(name);
  }
}

// Inheritance
class Employee extends Person {
  constructor(name, position) {
    super(name);
    this.position = position;
  }

  greet() {
    return `${super.greet()} I am a ${this.position}`;
  }
}

console.log(new Employee("Bo", "developer").greet());
// Hello, Bo I am a developer

While classes in JavaScript are still built on prototypes under the hood, the cleaner syntax dramatically improved code organization and readability.

Asynchronous Programming Revolution#

Promises: Taming Callback Hell#

Before ES6, asynchronous code (work that finishes later, like loading data) often led to deeply nested callbacks, affectionately known as "callback hell". In the sketches below, getUser, getFriends, getPhotos and displayPhotos stand for any functions that take a while to answer:

// Pre-ES6 callback hell (sketch: the helper functions are placeholders)
getUser(userId, function(user) {
  getFriends(user, function(friends) {
    getPhotos(friends, function(photos) {
      displayPhotos(photos, function() {
        console.log("Done!");
      });
    });
  });
});
// ES6 Promises (sketch: here the helpers return promises)
getUser(userId)
  .then(user => getFriends(user))
  .then(friends => getPhotos(friends))
  .then(photos => displayPhotos(photos))
  .then(() => console.log("Done!"))
  .catch(error => console.error("Error:", error));

A promise is an object that stands for a result that has not arrived yet. It later either succeeds (the next .then runs) or fails (.catch runs), and each .then can hand its result to the next one. That gives clear patterns for success, failure, and chaining, and it laid the groundwork for async/await in later JavaScript versions.

In a promise chain, which method runs when any earlier step fails?

The interactive comparison below runs both styles side by side so you can compare readability and error handling.

Promise vs Callback Hell
---html
<h1>Promise vs Callback Hell</h1>

<div class="intro">
	<p>This demo shows two approaches to handling asynchronous operations in JavaScript: traditional callbacks versus ES6 Promises. Both examples perform the same operations (user authentication, user data retrieval, and related posts), but with different code structures.</p>
</div>

<div class="app-container">
	<div class="panel callback-panel">
		<div class="panel-header">
			<h2 class="panel-title">Traditional Callbacks</h2>
		</div>

		<div class="code-section" id="callback-code">
			// Traditional nested callbacks
			function authenticateUser(username, password, callback) {
			setTimeout(() => {
			if (username === 'user' && password === 'pass') {
			callback(null, { id: 123, username: 'user' });
			} else {
			callback(new Error('Authentication failed'));
			}
			}, 1000);
			}

			function getUserData(userId, callback) {
			setTimeout(() => {
			const userData = {
			id: userId,
			name: 'John Doe',
			email: 'john@example.com'
			};
			callback(null, userData);
			}, 1000);
			}

			function getUserPosts(userId, callback) {
			setTimeout(() => {
			const posts = [
			{ id: 1, title: 'ES6 Is Great' },
			{ id: 2, title: 'Callbacks vs Promises' }
			];
			callback(null, posts);
			}, 1000);
			}

			// Usage with nested callbacks
			authenticateUser('user', 'pass', (authError, user) => {
			if (authError) {
			console.error('Auth error:', authError);
			return;
			}

			getUserData(user.id, (dataError, userData) => {
			if (dataError) {
			console.error('Data error:', dataError);
			return;
			}

			getUserPosts(userData.id, (postsError, posts) => {
			if (postsError) {
			console.error('Posts error:', postsError);
			return;
			}

			console.log('User:', userData);
			console.log('Posts:', posts);
			});
			});
			});</div>

		<div class="control-section">
			<div>
				<button id="run-callback" class="button callback-button">Run Callback Example</button>
				<span id="callback-spinner" class="spinner" style="display: none;"></span>
			</div>
			<div>
				<label>
					Auth fail?
					<input type="checkbox" id="callback-fail">
				</label>
			</div>
		</div>

		<div class="result-section">
			<div id="callback-result"></div>
			<button id="reset-callback" class="reset-button" style="display: none;">Reset</button>
		</div>
	</div>

	<div class="panel promise-panel">
		<div class="panel-header">
			<h2 class="panel-title">ES6 Promises</h2>
		</div>

		<div class="code-section" id="promise-code">
			// ES6 Promise-based approach
			function authenticateUser(username, password) {
			return new Promise((resolve, reject) => {
			setTimeout(() => {
			if (username === 'user' && password === 'pass') {
			resolve({ id: 123, username: 'user' });
			} else {
			reject(new Error('Authentication failed'));
			}
			}, 1000);
			});
			}

			function getUserData(userId) {
			return new Promise((resolve, reject) => {
			setTimeout(() => {
			const userData = {
			id: userId,
			name: 'John Doe',
			email: 'john@example.com'
			};
			resolve(userData);
			}, 1000);
			});
			}

			function getUserPosts(userId) {
			return new Promise((resolve, reject) => {
			setTimeout(() => {
			const posts = [
			{ id: 1, title: 'ES6 Is Great' },
			{ id: 2, title: 'Callbacks vs Promises' }
			];
			resolve(posts);
			}, 1000);
			});
			}

			// Usage with promise chain
			authenticateUser('user', 'pass')
			.then(user => {
			console.log('Authenticated:', user);
			return getUserData(user.id);
			})
			.then(userData => {
			console.log('User data:', userData);
			return getUserPosts(userData.id);
			})
			.then(posts => {
			console.log('User posts:', posts);
			})
			.catch(error => {
			console.error('Error:', error);
			});</div>

		<div class="control-section">
			<div>
				<button id="run-promise" class="button promise-button">Run Promise Example</button>
				<span id="promise-spinner" class="spinner" style="display: none;"></span>
			</div>
			<div>
				<label>
					Auth fail?
					<input type="checkbox" id="promise-fail">
				</label>
			</div>
		</div>

		<div class="result-section">
			<div id="promise-result"></div>
			<button id="reset-promise" class="reset-button" style="display: none;">Reset</button>
		</div>
	</div>
</div>

<div class="explanation">
	<h2>Why Promises Are Better</h2>
	<p>This example demonstrates why ES6 Promises offer significant advantages over nested callbacks:</p>
	<ol>
		<li><strong>Readability:</strong> The promise chain is linear and easier to follow than deeply nested callbacks.</li>
		<li><strong>Error handling:</strong> A single <code>.catch()</code> handles errors from any step, versus checking errors at each level.</li>
		<li><strong>Flow control:</strong> Promises enable cleaner sequential, parallel, or racing async operations.</li>
		<li><strong>Composability:</strong> Promise-returning functions can be easily composed and reused.</li>
		<li><strong>Guaranteed async:</strong> Promises always resolve in the next event loop, preventing timing issues.</li>
	</ol>
	<p>This foundation also enabled the even cleaner <code>async/await</code> syntax introduced in ES2017, which builds on top of Promises.</p>
</div>
---css
:root {
	--bg-color: #f5f5f5;
	--text-color: #333;
	--accent-color: #4a6da7;
	--primary-color: #4c7caf;
	--secondary-color: #a77a4a;
	--code-bg: #e0e0e0;
	--panel-bg: #fff;
	--border-color: #ccc;
	--success-color: #4caf50;
	--error-color: #f44336;
}

@media (prefers-color-scheme: dark) {
	:root {
		--bg-color: #1e1e1e;
		--text-color: #e0e0e0;
		--accent-color: #5b8ad6;
		--primary-color: #5b8ad6;
		--secondary-color: #d6975b;
		--code-bg: #2d2d2d;
		--panel-bg: #2a2a2a;
		--border-color: #444;
		--success-color: #66bb6a;
		--error-color: #ef5350;
	}
}

body {
	font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
	background-color: var(--bg-color);
	color: var(--text-color);
	line-height: 1.6;
	padding: 20px;
	max-width: 900px;
	margin: 0 auto;
	transition: background-color 0.3s, color 0.3s;
}

h1 {
	color: var(--accent-color);
	text-align: center;
	margin-bottom: 30px;
}

.intro {
	margin-bottom: 25px;
}

.app-container {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: 25px;
	margin-bottom: 30px;
}

@media (max-width: 768px) {
	.app-container {
		grid-template-columns: 1fr;
	}
}

.panel {
	background-color: var(--panel-bg);
	border-radius: 8px;
	box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
	padding: 20px;
	position: relative;
}

.panel-header {
	display: flex;
	justify-content: space-between;
	align-items: center;
	margin-bottom: 15px;
	border-bottom: 1px solid var(--border-color);
	padding-bottom: 10px;
}

.panel-title {
	font-size: 1.2rem;
	font-weight: bold;
	margin: 0;
}

.panel.callback-panel .panel-title {
	color: var(--secondary-color);
}

.panel.promise-panel .panel-title {
	color: var(--primary-color);
}

.code-section {
	background-color: var(--code-bg);
	border-radius: 5px;
	padding: 15px;
	margin-bottom: 20px;
	overflow-x: auto;
	font-family: "Courier New", Courier, monospace;
	white-space: pre;
	font-size: 0.9rem;
	line-height: 1.5;
}

.control-section {
	display: flex;
	justify-content: space-between;
	align-items: center;
	flex-wrap: wrap;
	gap: 10px;
}

.button {
	padding: 8px 15px;
	border: none;
	border-radius: 4px;
	cursor: pointer;
	font-weight: bold;
	transition: background-color 0.2s;
}

.callback-button {
	background-color: var(--secondary-color);
	color: white;
}

.promise-button {
	background-color: var(--primary-color);
	color: white;
}

.button:hover {
	opacity: 0.9;
}

.button:disabled {
	opacity: 0.5;
	cursor: not-allowed;
}

.result-section {
	margin-top: 15px;
	border-top: 1px dashed var(--border-color);
	padding-top: 15px;
}

.message {
	margin-bottom: 5px;
	padding: 5px;
	border-radius: 3px;
}

.success-message {
	color: var(--success-color);
}

.error-message {
	color: var(--error-color);
}

.reset-button {
	margin-top: 10px;
	background-color: var(--border-color);
	color: var(--text-color);
	padding: 5px 10px;
	border-radius: 3px;
	cursor: pointer;
	border: none;
	font-size: 0.8rem;
}

.reset-button:hover {
	opacity: 0.9;
}

.spinner {
	display: inline-block;
	width: 12px;
	height: 12px;
	border: 2px solid rgba(255, 255, 255, 0.3);
	border-radius: 50%;
	border-top-color: white;
	animation: spin 1s ease-in-out infinite;
	margin-left: 5px;
}

@keyframes spin {
	to {
		transform: rotate(360deg);
	}
}

.explanation {
	margin-top: 30px;
	background-color: var(--panel-bg);
	border-radius: 8px;
	padding: 20px;
	box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.explanation h2 {
	color: var(--accent-color);
	margin-top: 0;
}
---js
// DOM Elements
const callbackResultEl = document.getElementById("callback-result");
const promiseResultEl = document.getElementById("promise-result");
const runCallbackBtn = document.getElementById("run-callback");
const runPromiseBtn = document.getElementById("run-promise");
const resetCallbackBtn = document.getElementById("reset-callback");
const resetPromiseBtn = document.getElementById("reset-promise");
const callbackFailCheckbox = document.getElementById("callback-fail");
const promiseFailCheckbox = document.getElementById("promise-fail");
const callbackSpinner = document.getElementById("callback-spinner");
const promiseSpinner = document.getElementById("promise-spinner");

// Helper functions
function appendMessage(element, message, isError = false) {
  const msgEl = document.createElement("div");
  msgEl.classList.add("message");
  if (isError) {
    msgEl.classList.add("error-message");
  } else {
    msgEl.classList.add("success-message");
  }
  msgEl.textContent = message;
  element.appendChild(msgEl);
}

function resetResults(resultEl, resetBtn) {
  resultEl.innerHTML = "";
  resetBtn.style.display = "none";
}

function showResetButton(btn) {
  btn.style.display = "inline-block";
}

// Callback Implementation
function runCallbackExample() {
  resetResults(callbackResultEl, resetCallbackBtn);
  callbackSpinner.style.display = "inline-block";
  runCallbackBtn.disabled = true;

  const shouldFail = callbackFailCheckbox.checked;
  const username = shouldFail ? "wrong" : "user";

  // Implementation of the callback functions
  function authenticateUser(username, password, callback) {
    setTimeout(() => {
      appendMessage(callbackResultEl, "Authenticating user...");
      if (username === "user" && password === "pass") {
        const user = { id: 123, username: "user" };
        appendMessage(
        callbackResultEl,
        `Authentication successful: User ID: ${user.id}`);

        callback(null, user);
      } else {
        const error = new Error("Authentication failed");
        appendMessage(callbackResultEl, `Authentication failed!`, true);
        callback(error);
      }
    }, 1000);
  }

  function getUserData(userId, callback) {
    setTimeout(() => {
      appendMessage(callbackResultEl, `Fetching data for user ID: ${userId}...`);
      const userData = {
        id: userId,
        name: "John Doe",
        email: "john@example.com" };

      appendMessage(callbackResultEl, `User data received: ${userData.name}`);
      callback(null, userData);
    }, 1000);
  }

  function getUserPosts(userId, callback) {
    setTimeout(() => {
      appendMessage(callbackResultEl, `Fetching posts for user ID: ${userId}...`);
      const posts = [
      { id: 1, title: "ES6 Is Great" },
      { id: 2, title: "Callbacks vs Promises" }];

      appendMessage(callbackResultEl, `Received ${posts.length} posts`);
      callback(null, posts);
    }, 1000);
  }

  // Execute the nested callbacks
  authenticateUser(username, "pass", (authError, user) => {
    if (authError) {
      appendMessage(callbackResultEl, `Error: ${authError.message}`, true);
      callbackSpinner.style.display = "none";
      runCallbackBtn.disabled = false;
      showResetButton(resetCallbackBtn);
      return;
    }

    getUserData(user.id, (dataError, userData) => {
      if (dataError) {
        appendMessage(callbackResultEl, `Error: ${dataError.message}`, true);
        callbackSpinner.style.display = "none";
        runCallbackBtn.disabled = false;
        showResetButton(resetCallbackBtn);
        return;
      }

      getUserPosts(userData.id, (postsError, posts) => {
        if (postsError) {
          appendMessage(callbackResultEl, `Error: ${postsError.message}`, true);
          callbackSpinner.style.display = "none";
          runCallbackBtn.disabled = false;
          showResetButton(resetCallbackBtn);
          return;
        }

        appendMessage(callbackResultEl, `Operation complete with all data loaded!`);
        callbackSpinner.style.display = "none";
        runCallbackBtn.disabled = false;
        showResetButton(resetCallbackBtn);
      });
    });
  });
}

// Promise Implementation
function runPromiseExample() {
  resetResults(promiseResultEl, resetPromiseBtn);
  promiseSpinner.style.display = "inline-block";
  runPromiseBtn.disabled = true;

  const shouldFail = promiseFailCheckbox.checked;
  const username = shouldFail ? "wrong" : "user";

  // Implementation of the promise functions
  function authenticateUser(username, password) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        appendMessage(promiseResultEl, "Authenticating user...");
        if (username === "user" && password === "pass") {
          const user = { id: 123, username: "user" };
          appendMessage(
          promiseResultEl,
          `Authentication successful: User ID: ${user.id}`);

          resolve(user);
        } else {
          appendMessage(promiseResultEl, `Authentication failed!`, true);
          reject(new Error("Authentication failed"));
        }
      }, 1000);
    });
  }

  function getUserData(userId) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        appendMessage(promiseResultEl, `Fetching data for user ID: ${userId}...`);
        const userData = {
          id: userId,
          name: "John Doe",
          email: "john@example.com" };

        appendMessage(promiseResultEl, `User data received: ${userData.name}`);
        resolve(userData);
      }, 1000);
    });
  }

  function getUserPosts(userId) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        appendMessage(promiseResultEl, `Fetching posts for user ID: ${userId}...`);
        const posts = [
        { id: 1, title: "ES6 Is Great" },
        { id: 2, title: "Callbacks vs Promises" }];

        appendMessage(promiseResultEl, `Received ${posts.length} posts`);
        resolve(posts);
      }, 1000);
    });
  }

  // Execute the promise chain
  authenticateUser(username, "pass").
  then(user => {
    return getUserData(user.id);
  }).
  then(userData => {
    return getUserPosts(userData.id);
  }).
  then(posts => {
    appendMessage(promiseResultEl, `Operation complete with all data loaded!`);
  }).
  catch(error => {
    appendMessage(promiseResultEl, `Error: ${error.message}`, true);
  }).
  finally(() => {
    promiseSpinner.style.display = "none";
    runPromiseBtn.disabled = false;
    showResetButton(resetPromiseBtn);
  });
}

// Event Listeners
runCallbackBtn.addEventListener("click", runCallbackExample);
runPromiseBtn.addEventListener("click", runPromiseExample);
resetCallbackBtn.addEventListener("click", () =>
resetResults(callbackResultEl, resetCallbackBtn));

resetPromiseBtn.addEventListener("click", () =>
resetResults(promiseResultEl, resetPromiseBtn));

Modules and Data Structures#

ES Modules: Solving the Module Problem#

Before ES6, JavaScript lacked a standard module system, leading to competing solutions like AMD, CommonJS, and UMD:

// Pre-ES6 CommonJS modules (Node.js), in a file such as app.js
var helpers = require('./helpers');
module.exports = {
  doSomething: function() { /* ... */ }
};
// ES6 modules, in a file such as app.js (not runnable on its own)
import { formatDate, calculateTotal } from './helpers.js';
export function doSomething() { /* ... */ }
export default class MainComponent { /* ... */ }

Either way, a module is a file with its own private variables that shares only what it exports.

ES6 modules provided a clean, standardized way to organize code, with static imports and exports that could be analyzed and optimized by tools.

ES Modules represent JavaScript's official standardized module system – a solution to years of fragmented approaches. The interactive file explorer below demonstrates how modules work together in a typical project, showing different export and import patterns. Click on different files to explore the module interactions and see how ES6's module system brings organization and clarity to modern JavaScript applications.

ES Modules Explorer
---html
<header>
	<h1>ES Modules Explorer</h1>
	<div class="subtitle">Understanding Modern JavaScript Module System</div>
</header>

<div class="container">
	<div class="intro">
		<p>ES Modules (ESM) were introduced in ES6 and represent JavaScript's official standard for packaging and reusing code. This explorer helps you understand how modules work, their key features, and best practices for using them in modern web development.</p>
		<p>Before ES6, JavaScript lacked a native module system, leading to various workarounds like CommonJS (Node.js), AMD (RequireJS), and UMD. ES Modules solve these issues with a standardized approach that works both in the browser and Node.js environments.</p>
	</div>

	<h2>Module Systems Comparison</h2>

	<table class="comparison-table">
		<tr>
			<th>Feature</th>
			<th>ES Modules</th>
			<th>CommonJS (Node.js)</th>
			<th>AMD (RequireJS)</th>
		</tr>
		<tr>
			<td>Syntax</td>
			<td><code>import</code> / <code>export</code></td>
			<td><code>require()</code> / <code>module.exports</code></td>
			<td><code>define()</code> / <code>require()</code></td>
		</tr>
		<tr>
			<td>Loading</td>
			<td>Static (parsed at compile time)</td>
			<td>Dynamic (at runtime)</td>
			<td>Dynamic (at runtime)</td>
		</tr>
		<tr>
			<td>Browser Support</td>
			<td>Native (modern browsers)</td>
			<td>Requires bundler</td>
			<td>Yes (with library)</td>
		</tr>
		<tr>
			<td>Tree Shaking</td>
			<td>Yes (unused exports can be removed)</td>
			<td>No</td>
			<td>No</td>
		</tr>
		<tr>
			<td>Circular Dependencies</td>
			<td>Handled with live bindings</td>
			<td>Partial exports available</td>
			<td>Handled with callbacks</td>
		</tr>
	</table>

	<h2>Exploring a Module-Based Project</h2>

	<div class="file-system">
		<div class="file-explorer">
			<div class="directory">📁 src</div>
			<div class="file-item file" data-file="main">📄 main.js</div>
			<div class="file-item file" data-file="utils">📄 utils.js</div>
			<div class="file-item file" data-file="math">📄 math.js</div>
			<div class="file-item file" data-file="user">📄 user.js</div>
			<div class="directory">📁 components</div>
			<div class="file-item file" data-file="button">📄 components/button.js</div>
			<div class="file-item file" data-file="card">📄 components/card.js</div>
			<div class="file-item file" data-file="index">📄 components/index.js</div>
			<div class="file-item file" data-file="html">📄 index.html</div>
		</div>

		<div class="file-viewer">
			<div class="file-name" id="current-file">Select a file to view</div>
			<pre class="code-block" id="file-code">// File content will appear here</pre>
		</div>
	</div>

	<h2>Key Benefits of ES Modules</h2>

	<div class="features">
		<div class="feature-card">
			<div class="feature-header">
				<h3 class="feature-title">Static Analysis</h3>
			</div>
			<div class="feature-content">
				<p>Import/export statements are analyzed at compile time, enabling:</p>
				<ul>
					<li><strong>Tree-shaking:</strong> Bundlers can remove unused exports</li>
					<li><strong>Early error detection:</strong> Import errors found before execution</li>
					<li><strong>Better IDE support:</strong> Improved auto-completion and code navigation</li>
				</ul>
				<div class="code-block">// Error detected at parse time, not runtime
					import { nonExistentFunction } from './utils.js';</div>
			</div>
		</div>

		<div class="feature-card">
			<div class="feature-header">
				<h3 class="feature-title">Scoped Modules</h3>
			</div>
			<div class="feature-content">
				<p>Each module has its own scope, preventing global namespace pollution.</p>
				<div class="code-block">// variables.js
					const counter = 0;
					export { counter };

					// main.js
					const counter = 42; // No conflict with imported counter
					import { counter as importedCounter } from './variables.js';

					console.log(counter); // 42
					console.log(importedCounter); // 0</div>
				<p>Variables declared in a module aren't added to the global scope, reducing bugs and conflicts.</p>
			</div>
		</div>

		<div class="feature-card">
			<div class="feature-header">
				<h3 class="feature-title">Single Instance</h3>
			</div>
			<div class="feature-content">
				<p>Modules are singletons - imported only once regardless of how many times they're imported.</p>
				<div class="code-block">// counter.js
					export let count = 0;
					export function increment() {
					count++;
					}

					// moduleA.js
					import { count, increment } from './counter.js';

					// moduleB.js
					import { count, increment } from './counter.js';
					increment();
					console.log(count); // 1

					// Back in moduleA.js
					console.log(count); // Also 1, same instance!</div>
				<p>This ensures consistent state across your application and optimizes memory usage.</p>
			</div>
		</div>

		<div class="feature-card">
			<div class="feature-header">
				<h3 class="feature-title">Live Bindings</h3>
			</div>
			<div class="feature-content">
				<p>Exported values remain connected to their source variables, not copied.</p>
				<div class="code-block">// config.js
					export let apiUrl = 'https://api.example.com/v1';

					export function switchToDevEnvironment() {
					apiUrl = 'https://dev.example.com/v1';
					}

					// service.js
					import { apiUrl, switchToDevEnvironment } from './config.js';

					console.log(apiUrl); // https://api.example.com/v1

					switchToDevEnvironment();

					console.log(apiUrl); // https://dev.example.com/v1</div>
				<p>This feature enables dynamic updating of values across modules without reloading.</p>
			</div>
		</div>
	</div>

	<h2>Common Module Patterns</h2>

	<div class="examples">
		<div class="example-card">
			<div class="example-header">
				<h3 class="example-title">Named Exports & Imports</h3>
			</div>
			<div class="example-content">
				<div class="code-block">// utils.js
					export function formatDate(date) {
					return new Date(date).toLocaleDateString();
					}

					export function capitalize(string) {
					return string.charAt(0).toUpperCase()
					+ string.slice(1);
					}

					// In another file
					import { formatDate, capitalize } from './utils.js';

					const date = formatDate(new Date());
					const name = capitalize('alice');</div>
				<p>Use named exports when a module provides multiple independent functions or values.</p>
			</div>
		</div>

		<div class="example-card">
			<div class="example-header">
				<h3 class="example-title">Default Exports & Imports</h3>
			</div>
			<div class="example-content">
				<div class="code-block">// user.js
					export default class User {
					constructor(name, email) {
					this.name = name;
					this.email = email;
					}

					getProfile() {
					return `${this.name} (${this.email})`;
					}
					}

					// In another file
					import User from './user.js';

					const alice = new User('Alice', 'alice@example.com');</div>
				<p>Use default exports when a module primarily provides a single main value, like a class or function.</p>
			</div>
		</div>

		<div class="example-card">
			<div class="example-header">
				<h3 class="example-title">Mixed Exports</h3>
			</div>
			<div class="example-content">
				<div class="code-block">// api.js
					export default class API {
					constructor(baseURL) {
					this.baseURL = baseURL;
					}

					async get(endpoint) {
					// Implementation
					}
					}

					export const DEFAULT_URL = 'https://api.example.com';
					export const API_VERSION = 'v1';

					// In another file
					import API, { DEFAULT_URL, API_VERSION } from './api.js';</div>
				<p>Modules can have both default and named exports. Default is imported without braces, named exports with braces.</p>
			</div>
		</div>

		<div class="example-card">
			<div class="example-header">
				<h3 class="example-title">Namespace Imports</h3>
			</div>
			<div class="example-content">
				<div class="code-block">// math.js
					export function add(a, b) { return a + b; }
					export function subtract(a, b) { return a - b; }
					export function multiply(a, b) { return a * b; }
					export function divide(a, b) { return a / b; }

					// In another file
					import * as Math from './math.js';

					const sum = Math.add(5, 3);
					const product = Math.multiply(4, 2);</div>
				<p>Namespace imports let you import all exports from a module as properties of a single object.</p>
			</div>
		</div>
	</div>

	<h2>Advanced Module Features</h2>

	<div class="examples">
		<div class="example-card">
			<div class="example-header">
				<h3 class="example-title">Re-exporting</h3>
			</div>
			<div class="example-content">
				<div class="code-block">// components/index.js
					export { default as Button } from './button.js';
					export { default as Card } from './card.js';

					// In another file
					import { Button, Card } from './components';</div>
				<p>Re-exporting lets you consolidate and organize exports from multiple modules.</p>
			</div>
		</div>

		<div class="example-card">
			<div class="example-header">
				<h3 class="example-title">Dynamic Imports</h3>
			</div>
			<div class="example-content">
				<div class="code-block">// Load modules conditionally
					async function loadFeature() {
					if (user.premium) {
					const { PremiumFeature } = await import('./premium.js');
					return new PremiumFeature();
					} else {
					const { BasicFeature } = await import('./basic.js');
					return new BasicFeature();
					}
					}</div>
				<p>Dynamic imports allow for conditional loading of modules, supporting code-splitting and lazy-loading.</p>
			</div>
		</div>

		<div class="example-card">
			<div class="example-header">
				<h3 class="example-title">Module Aliases & Renaming</h3>
			</div>
			<div class="example-content">
				<div class="code-block">// Rename during import
					import {
					add as sum,
					multiply as product
					} from './math.js';

					// Rename during export
					export {
					formatDate as formatDateString,
					formatTime
					} from './formatters.js';</div>
				<p>Renaming allows you to avoid naming conflicts or provide more context-appropriate names.</p>
			</div>
		</div>
	</div>

	<div class="tip-box">
		<div class="tip-title">💡 Best Practices</div>
		<ul>
			<li><strong>Prefer named exports for utility/helper modules</strong> with multiple functions</li>
			<li><strong>Use default exports for modules</strong> representing a single main concept</li>
			<li><strong>Keep modules focused</strong> on a single responsibility</li>
			<li><strong>Structure your code</strong> to avoid deep dependency chains</li>
			<li><strong>Consider using barrel files</strong> (index.js) to simplify import paths</li>
			<li><strong>Use dynamic imports</strong> for code-splitting and conditional loading</li>
			<li><strong>Be consistent with your export style</strong> across your project</li>
		</ul>
	</div>
</div>

<footer>
	<p>ES Modules are the standard module system, and they are worth learning early. Static imports let tools see your dependencies without running the code.</p>
</footer>

<!-- 
    Fallback for browsers that don't support ES modules
    It will only load if modules aren't supported
  -->
<script nomodule>
	alert('Your browser does not support ES Modules. Please update to a modern browser.');
</script>
---css
:root {
	--bg-color: #f8f9fa;
	--text-color: #212529;
	--primary-color: #4b6bab;
	--secondary-color: #5a8a7a;
	--accent-color: #d35e56;
	--light-bg: #ffffff;
	--dark-bg: #343a40;
	--border-color: #dee2e6;
	--code-bg: #f5f5f5;
	--file-bg: #f0f0f0;
	--active-tab: #e2e6ea;
}

@media (prefers-color-scheme: dark) {
	:root {
		--bg-color: #2d3748;
		--text-color: #e2e8f0;
		--primary-color: #63b3ed;
		--secondary-color: #68d391;
		--accent-color: #fc8181;
		--light-bg: #4a5568;
		--dark-bg: #1a202c;
		--border-color: #4a5568;
		--code-bg: #2d3748;
		--file-bg: #2d3748;
		--active-tab: #4a5568;
	}
}

body {
	font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
	background-color: var(--bg-color);
	color: var(--text-color);
	line-height: 1.6;
	margin: 0;
	padding: 0;
}

header {
	background-color: var(--primary-color);
	color: white;
	padding: 20px;
	text-align: center;
	box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

h1 {
	margin: 0;
	font-size: 1.8rem;
}

.subtitle {
	margin-top: 10px;
	font-size: 1rem;
	opacity: 0.9;
}

.container {
	max-width: 1200px;
	margin: 0 auto;
	padding: 20px;
}

.intro {
	background-color: var(--light-bg);
	padding: 20px;
	border-radius: 8px;
	margin-bottom: 30px;
	box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}

.comparison-table {
	width: 100%;
	border-collapse: collapse;
	margin-bottom: 20px;
	background-color: var(--light-bg);
	border-radius: 8px;
	overflow: hidden;
	box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}

.comparison-table th,
.comparison-table td {
	padding: 15px;
	text-align: left;
	border-bottom: 1px solid var(--border-color);
}

.comparison-table th {
	background-color: var(--primary-color);
	color: white;
}

.comparison-table tr:last-child td {
	border-bottom: none;
}

.comparison-table tr:nth-child(even) {
	background-color: rgba(0, 0, 0, 0.02);
}

@media (prefers-color-scheme: dark) {
	.comparison-table tr:nth-child(even) {
		background-color: rgba(255, 255, 255, 0.02);
	}
}

.module-viewer {
	display: flex;
	flex-direction: column;
	background-color: var(--light-bg);
	border-radius: 8px;
	overflow: hidden;
	box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
	margin-bottom: 30px;
}

.file-tabs {
	display: flex;
	background-color: var(--dark-bg);
}

.file-tab {
	padding: 10px 15px;
	background-color: var(--dark-bg);
	color: white;
	border: none;
	cursor: pointer;
	transition: background-color 0.2s;
}

.file-tab:hover {
	background-color: rgba(255, 255, 255, 0.1);
}

.file-tab.active {
	background-color: var(--active-tab);
	color: var(--text-color);
}

.file-content {
	display: none;
	padding: 15px;
}

.file-content.active {
	display: block;
}

.code-block {
	background-color: var(--code-bg);
	padding: 15px;
	border-radius: 4px;
	overflow-x: auto;
	font-family: "Courier New", Courier, monospace;
	white-space: pre;
	line-height: 1.5;
}

.examples {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
	gap: 20px;
	margin-bottom: 30px;
}

.example-card {
	background-color: var(--light-bg);
	border-radius: 8px;
	overflow: hidden;
	box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}

.example-header {
	background-color: var(--secondary-color);
	color: white;
	padding: 15px;
}

.example-title {
	margin: 0;
	font-size: 1.2rem;
}

.example-content {
	padding: 15px;
}

.highlight {
	color: var(--accent-color);
	font-weight: bold;
}

.file-system {
	display: flex;
	margin-bottom: 30px;
}

.file-explorer {
	width: 250px;
	background-color: var(--file-bg);
	border-radius: 8px 0 0 8px;
	padding: 15px;
}

.file-item {
	margin-bottom: 5px;
	padding: 8px 10px;
	cursor: pointer;
	border-radius: 4px;
	transition: background-color 0.2s;
}

.file-item:hover {
	background-color: rgba(0, 0, 0, 0.05);
}

@media (prefers-color-scheme: dark) {
	.file-item:hover {
		background-color: rgba(255, 255, 255, 0.05);
	}
}

.file-item.selected {
	background-color: var(--primary-color);
	color: white;
}

.file-viewer {
	flex-grow: 1;
	background-color: var(--light-bg);
	border-radius: 0 8px 8px 0;
	padding: 15px;
}

.file-name {
	font-weight: bold;
	margin-bottom: 10px;
	padding-bottom: 10px;
	border-bottom: 1px solid var(--border-color);
}

.code-comment {
	color: #6a9955;
}

@media (prefers-color-scheme: dark) {
	.code-comment {
		color: #6a9;
	}
}

.tip-box {
	background-color: rgba(74, 107, 171, 0.1);
	border-left: 4px solid var(--primary-color);
	padding: 15px;
	margin: 20px 0;
	border-radius: 0 4px 4px 0;
}

.tip-title {
	font-weight: bold;
	margin-bottom: 8px;
	color: var(--primary-color);
}

.directory {
	font-weight: bold;
	margin-top: 10px;
}

.file-item.file {
	padding-left: 25px;
}

footer {
	text-align: center;
	padding: 20px;
	margin-top: 30px;
	border-top: 1px solid var(--border-color);
}

@media (max-width: 768px) {
	.file-system {
		flex-direction: column;
	}

	.file-explorer {
		width: 100%;
		border-radius: 8px 8px 0 0;
	}

	.file-viewer {
		border-radius: 0 0 8px 8px;
	}
}

:root {
	--bg-color: #f8f9fa;
	--text-color: #212529;
	--primary-color: #4b6bab;
	--secondary-color: #5a8a7a;
	--accent-color: #d35e56;
	--light-bg: #ffffff;
	--dark-bg: #343a40;
	--border-color: #dee2e6;
	--code-bg: #f5f5f5;
	--file-bg: #f0f0f0;
	--active-tab: #e2e6ea;
	--keyword-color: #569cd6;
	--string-color: #ce9178;
	--function-color: #dcdcaa;
	--comment-color: #6a9955;
}

@media (prefers-color-scheme: dark) {
	:root {
		--bg-color: #2d3748;
		--text-color: #e2e8f0;
		--primary-color: #63b3ed;
		--secondary-color: #68d391;
		--accent-color: #fc8181;
		--light-bg: #4a5568;
		--dark-bg: #1a202c;
		--border-color: #4a5568;
		--code-bg: #2d3748;
		--file-bg: #2d3748;
		--active-tab: #4a5568;
		--keyword-color: #569cd6;
		--string-color: #ce9178;
		--function-color: #dcdcaa;
		--comment-color: #6a9955;
	}
}

body {
	font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
	background-color: var(--bg-color);
	color: var(--text-color);
	line-height: 1.6;
	margin: 0;
	padding: 0;
}

header {
	background-color: var(--primary-color);
	color: white;
	padding: 20px;
	text-align: center;
	box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

h1 {
	margin: 0;
	font-size: 1.8rem;
}

.subtitle {
	margin-top: 10px;
	font-size: 1rem;
	opacity: 0.9;
}

.container {
	max-width: 1200px;
	margin: 0 auto;
	padding: 20px;
}

.intro {
	background-color: var(--light-bg);
	padding: 20px;
	border-radius: 8px;
	margin-bottom: 30px;
	box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}

.comparison-table {
	width: 100%;
	border-collapse: collapse;
	margin-bottom: 20px;
	background-color: var(--light-bg);
	border-radius: 8px;
	overflow: hidden;
	box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}

.comparison-table th,
.comparison-table td {
	padding: 15px;
	text-align: left;
	border-bottom: 1px solid var(--border-color);
}

.comparison-table th {
	background-color: var(--primary-color);
	color: white;
}

.comparison-table tr:last-child td {
	border-bottom: none;
}

.comparison-table tr:nth-child(even) {
	background-color: rgba(0, 0, 0, 0.02);
}

@media (prefers-color-scheme: dark) {
	.comparison-table tr:nth-child(even) {
		background-color: rgba(255, 255, 255, 0.02);
	}
}

.file-system {
	display: flex;
	margin-bottom: 30px;
	background-color: var(--light-bg);
	border-radius: 8px;
	overflow: hidden;
	box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}

.file-explorer {
	width: 250px;
	background-color: var(--file-bg);
	padding: 15px;
}

.file-item {
	margin-bottom: 5px;
	padding: 8px 10px;
	cursor: pointer;
	border-radius: 4px;
	transition: background-color 0.2s;
}

.file-item:hover {
	background-color: rgba(0, 0, 0, 0.05);
}

@media (prefers-color-scheme: dark) {
	.file-item:hover {
		background-color: rgba(255, 255, 255, 0.05);
	}
}

.file-item.selected {
	background-color: var(--primary-color);
	color: white;
}

.file-viewer {
	flex-grow: 1;
	padding: 15px;
	overflow: hidden;
}

.file-name {
	font-weight: bold;
	margin-bottom: 10px;
	padding-bottom: 10px;
	border-bottom: 1px solid var(--border-color);
}

pre.code-block {
	font-family: "Courier New", Courier, monospace;
	white-space: pre-wrap;
	overflow-x: auto;
	background-color: var(--code-bg);
	padding: 15px;
	border-radius: 4px;
	font-size: 14px;
	line-height: 1.5;
}

.keyword {
	color: var(--keyword-color);
}

.string {
	color: var(--string-color);
}

.function {
	color: var(--function-color);
}

.comment {
	color: var(--comment-color);
}

.features {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
	gap: 20px;
	margin-bottom: 30px;
}

.feature-card {
	background-color: var(--light-bg);
	border-radius: 8px;
	overflow: hidden;
	box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}

.feature-header {
	background-color: var(--secondary-color);
	color: white;
	padding: 15px;
}

.feature-title {
	margin: 0;
	font-size: 1.2rem;
}

.feature-content {
	padding: 15px;
}

.examples {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
	gap: 20px;
	margin-bottom: 30px;
}

.example-card {
	background-color: var(--light-bg);
	border-radius: 8px;
	overflow: hidden;
	box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}

.example-header {
	background-color: var(--secondary-color);
	color: white;
	padding: 15px;
}

.example-title {
	margin: 0;
	font-size: 1.2rem;
}

.example-content {
	padding: 15px;
}

.highlight {
	color: var(--accent-color);
	font-weight: bold;
}

.tip-box {
	background-color: rgba(74, 107, 171, 0.1);
	border-left: 4px solid var(--primary-color);
	padding: 15px;
	margin: 20px 0;
	border-radius: 0 4px 4px 0;
}

.tip-title {
	font-weight: bold;
	margin-bottom: 8px;
	color: var(--primary-color);
}

.directory {
	font-weight: bold;
	margin-top: 10px;
}

.file-item.file {
	padding-left: 25px;
}

footer {
	text-align: center;
	padding: 20px;
	margin-top: 30px;
	border-top: 1px solid var(--border-color);
}

@media (max-width: 768px) {
	.file-system {
		flex-direction: column;
	}

	.file-explorer {
		width: 100%;
	}
}

code {
	background-color: var(--code-bg);
	padding: 2px 4px;
	border-radius: 3px;
	font-family: "Courier New", Courier, monospace;
}
---js
// File content data
const files = {
  main: {
    name: "main.js",
    content: `// Main application entry point
import User from './user.js';
import { formatDate, capitalize } from './utils.js';
import * as Math from './math.js';
import { Button, Card } from './components/index.js';

// Create user
const user = new User('alice', 'alice@example.com');
console.log(\`Welcome, \${capitalize(user.name)}!\`);

// Calculate and format order details
const orderDate = new Date();
const formattedDate = formatDate(orderDate);
const totalItems = 5;
const price = 9.99;
const total = Math.multiply(totalItems, price);

console.log(\`Order Date: \${formattedDate}\`);
console.log(\`Order Total: $\${total.toFixed(2)}\`);

// Initialize UI components
const loginButton = new Button('Login', () => alert('Login clicked'));
const productCard = new Card('ES6 in Action', 'Master modern JavaScript features!');

document.body.appendChild(loginButton.render());
document.body.appendChild(productCard.render());` },

  utils: {
    name: "utils.js",
    content: `// Utility functions
export function formatDate(date) {
  return new Date(date).toLocaleDateString();
}

export function formatTime(date) {
  return new Date(date).toLocaleTimeString();
}

export function capitalize(string) {
  return string.charAt(0).toUpperCase() + string.slice(1);
}

export function debounce(func, wait) {
  let timeout;
  return function(...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, args), wait);
  };
}

// Note that all exports are "named exports" in this module
// This is ideal for utility modules with multiple small functions` },

  math: {
    name: "math.js",
    content: `// Math operations module
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

export function multiply(a, b) {
  return a * b;
}

export function divide(a, b) {
  if (b === 0) {
    throw new Error('Division by zero');
  }
  return a / b;
}

export const PI = 3.14159265359;

// We could also write this as:
// const math = { add, subtract, multiply, divide, PI };
// export default math;
// But named exports are better for tree-shaking` },

  user: {
    name: "user.js",
    content: `// User model
export default class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
    this.createdAt = new Date();
  }
  
  getProfile() {
    return {
      name: this.name,
      email: this.email,
      createdAt: this.createdAt
    };
  }
  
  toString() {
    return \`\${this.name} (\${this.email})\`;
  }
}

// This module uses a default export because it primarily
// provides a single main thing (the User class)

// We could also add named exports for related functionality:
export const validateEmail = (email) => {
  const re = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
  return re.test(email);
};` },

  button: {
    name: "components/button.js",
    content: `// Button component
export default class Button {
  constructor(text, onClick) {
    this.text = text;
    this.onClick = onClick;
  }
  
  render() {
    const button = document.createElement('button');
    button.textContent = this.text;
    button.addEventListener('click', this.onClick);
    button.className = 'button';
    
    return button;
  }
}

// Private helper function (not exported)
function validateText(text) {
  if (typeof text !== 'string') {
    throw new Error('Button text must be a string');
  }
  return text.trim();
}

// Note: Functions/variables that aren't exported
// stay private to the module` },

  card: {
    name: "components/card.js",
    content: `// Card component
export default class Card {
  constructor(title, content) {
    this.title = title;
    this.content = content;
  }
  
  render() {
    const card = document.createElement('div');
    card.className = 'card';
    
    const titleEl = document.createElement('h3');
    titleEl.textContent = this.title;
    
    const contentEl = document.createElement('p');
    contentEl.textContent = this.content;
    
    card.appendChild(titleEl);
    card.appendChild(contentEl);
    
    return card;
  }
}

// Component-specific styles that aren't exported
export const cardStyles = \`
  .card {
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 15px;
    margin-bottom: 15px;
  }
  
  .card h3 {
    margin-top: 0;
  }
\`;` },

  index: {
    name: "components/index.js",
    content: `// Barrel file - re-exports all components
// This pattern simplifies imports for consumers

// Re-export each component as a named export
export { default as Button } from './button.js';
export { default as Card } from './card.js';

// We could also include component-specific exports
export { cardStyles } from './card.js';

/* With this barrel file, consumers can do:
   import { Button, Card } from './components';
   
   Instead of:
   import Button from './components/button.js';
   import Card from './components/card.js';
*/` },

  html: {
    name: "index.html",
    content: `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ES Modules Demo</title>
  <style>
    body {
      font-family: sans-serif;
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
    }
    
    .button {
      padding: 8px 16px;
      background-color: #4a6fa5;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <h1>ES Modules Demo</h1>
  
  <!-- 
    Note the type="module" attribute - this tells the browser 
    to load this script as an ES module
  -->
  <script type="module" src="src/main.js">` } };



// File selection handling
document.querySelectorAll(".file-item").forEach(item => {
  item.addEventListener("click", function () {
    // Get file key
    const fileKey = this.getAttribute("data-file");
    if (!files[fileKey]) return;

    // Update file viewer
    document.getElementById("current-file").textContent = files[fileKey].name;
    document.getElementById("file-code").textContent = files[fileKey].content;

    // Update selected state
    document.querySelectorAll(".file-item").forEach(el => {
      el.classList.remove("selected");
    });
    this.classList.add("selected");
  });
});

// Select main.js by default
document.querySelector('[data-file="main"]').click();

New Data Structures: Beyond Objects and Arrays#

ES6 introduced specialized collections that addressed limitations of regular objects and arrays. A Map is key-value pairs where a key can be anything, including an object. A Set keeps each value only once:

const userMap = new Map();
const key = {id: 1};
userMap.set(key, {name: "Alice"});  // an object as a key
console.log(userMap.get(key));      // { name: 'Alice' }
console.log(userMap.get({id: 1}));  // undefined (a different object, even with the same contents)

const uniqueIds = new Set([1, 2, 3, 1, 2]);
console.log(uniqueIds);             // Set(3) { 1, 2, 3 }

The undefined line is the thing to notice: a Map compares object keys by identity (is it the very same object?), not by what is inside. A plain object would turn both keys into the same string, "[object Object]".

What does new Set([1, 2, 3, 1, 2]).size return?

WeakMap and WeakSet are variants whose keys do not stop the garbage collector from freeing an object; you will rarely need them as a beginner.

Iterators and Generators: A Preview#

This section is a short preview, not a full lesson. An iterable is anything you can loop over with for...of, and ES6 made that a standard protocol (the iterator protocol): an iterable hands out values one at a time, and each request returns { value, done }. Strings and arrays already work:

for (const char of "hello") {
  console.log(char);  // h, e, l, l, o (one per line)
}

A generator is a function you can pause. You write it with function*, and each yield hands back one value and pauses until the next one is asked for:

function* numberGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

const it = numberGenerator();
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
console.log(it.next()); // { value: undefined, done: true }

for (const num of numberGenerator()) {
  console.log(num);  // 1, 2, 3 (one per line)
}

for...of simply calls next() until done is true. You can make your own object loopable by giving it a generator under the special key Symbol.iterator:

const myIterable = {
  *[Symbol.iterator]() {
    yield "first";
    yield "second";
  }
};
console.log([...myIterable]); // [ 'first', 'second' ]
What does done equal on the next() call that comes after the last yield?

Other Notable Additions#

Default Parameters, Rest and Spread#

ES6 introduced several small but powerful features that made function definitions and array/object manipulation more intuitive:

// Default parameters
function greet(name = "Guest") {
  return `Hello, ${name}!`;
}
console.log(greet());        // Hello, Guest!
console.log(greet("Alice")); // Hello, Alice!

// Rest parameters
function collectItems(first, second, ...others) {
  console.log(others);  // Array of remaining arguments
}
collectItems(1, 2, 3, 4); // [ 3, 4 ]

// Spread operator
const numbers = [1, 2, 3];
const expanded = [...numbers, 4, 5];  // [1, 2, 3, 4, 5]

// Combining objects (ES2018 enhancement)
const defaults = { theme: "dark", size: "medium" };
const userSettings = { size: "large" };
const combined = { ...defaults, ...userSettings };  // Overrides size

The Impact on the Ecosystem#

The release of ES6 triggered a cascade of changes throughout the JavaScript ecosystem:

  1. Reduced reliance on utility libraries: Features like arrow functions, template literals, and promises reduced the need for libraries like Underscore or jQuery.

  2. Framework evolution: React, Angular, and Vue embraced ES6 features, leading to more declarative and component-based architectures.

  3. Developer experience improvements: Better debugging, more readable code, and reduced boilerplate made developers more productive.

Looking Forward#

ES6 was just the beginning. It set the stage for an accelerated release cycle, with yearly updates bringing features like async/await (ES2017), rest/spread for objects (ES2018), and optional chaining (ES2020).

Conclusion#

Code written after ES6 looks quite different from code written before it. Most of what came later builds on the same pieces: let and const, arrow functions, classes, promises and modules.

There is a problem, though. ES6 gave us a lot of syntax, but most real programs spend their time reshaping data: filtering a list, copying an object, turning pairs back into an object. The next article picks that up with the small additions from ES2018 and ES2019.

Comments require JavaScript.